diff --git a/drafts/draft-lcurley-moq-lite.md b/drafts/draft-lcurley-moq-lite.md index 358b3bcfc89..926eaa10ef5 100644 --- a/drafts/draft-lcurley-moq-lite.md +++ b/drafts/draft-lcurley-moq-lite.md @@ -441,6 +441,7 @@ A second ANNOUNCE_START for an already-advertised route is a protocol violation; The subscriber MUST close the session with a PROTOCOL_VIOLATION if it receives an ANNOUNCE_END, ANNOUNCE_UPDATE, or ANNOUNCE_RESTART referencing an Announce ID that was never assigned or already retired, an ANNOUNCE_START for a route that is already advertised, or any announcement before ANNOUNCE_OK. When the stream is closed, the subscriber MUST assume that all routes are now unavailable. +A broadcast path has at most 32 segments and at most 16,384 bytes; a receiver MUST close the session with a PROTOCOL_VIOLATION on a longer one, including one resolved by [Compression](#announce-compression). A route covers a path when its prefix is a leading run of the path's segments; matching is per path segment, so a prefix never matches half a segment, and equality is byte-by-byte within each segment. A publisher answering a request stream presents each of its routes clamped to the intersection with the requested prefix: a route above the request's prefix appears as the request prefix itself (an empty suffix), which is exactly the covered set the subscriber may see. There MAY be multiple Announce Streams, potentially containing overlapping prefixes, that get their own ANNOUNCE_OK + announcements. @@ -1824,6 +1825,8 @@ The `Message Length` describes the payload size on the wire. ## moq-lite-07 +- Limited a broadcast path to 32 segments and 16,384 bytes. + - A session without a negotiated subprotocol negotiates the version in a draft-14 CLIENT_SETUP and SERVER_SETUP exchange. Implementations apply this to moq-lite-03 through moq-lite-06 too. - Added the SETUP `Implementation` parameter (0x6): the sender's software as `name/version` product tokens. Earlier versions with a Setup Stream ignore it as an unknown parameter. diff --git a/js/net/api/index.api.md b/js/net/api/index.api.md index 6d910943399..341dd9c39dc 100644 --- a/js/net/api/index.api.md +++ b/js/net/api/index.api.md @@ -427,6 +427,8 @@ function isWebTransportSupported(): boolean; function join(path: Valid, other: Valid): Valid; +const MAX_BYTES = 16384; + const MAX_GROUP_CACHE_BYTES: number; const MAX_GROUP_FRAMES = 8192; @@ -524,6 +526,7 @@ declare namespace Path { Valid, Relative, MAX_PARTS, + MAX_BYTES, compareSpecificity, InvalidPattern, Pattern, diff --git a/js/net/src/lite/announce.test.ts b/js/net/src/lite/announce.test.ts index 9263e6bbc10..84d4ce421d4 100644 --- a/js/net/src/lite/announce.test.ts +++ b/js/net/src/lite/announce.test.ts @@ -390,6 +390,17 @@ test("AnnounceHistory rejects a keep longer than the base", () => { expect(() => history.update({ id: 0n, hops: [], hopBase: { distance: 1n, keep: 2 } })).toThrow(ProtocolViolation); }); +test("AnnounceHistory rejects a resolved path past the byte limit", () => { + const history = new AnnounceHistory(); + history.start({ suffix: Path.from("x".repeat(Path.MAX_BYTES - 2)), hops: [] }); + // `head/y` is exactly the limit, and one byte more is refused. + const { suffix } = history.start({ suffix: Path.from("y"), hops: [], pathBase: { distance: 1n, keep: 1 } }); + expect(suffix.length).toBe(Path.MAX_BYTES); + expect(() => history.start({ suffix: Path.from("yz"), hops: [], pathBase: { distance: 1n, keep: 1 } })).toThrow( + ProtocolViolation, + ); +}); + test("AnnounceHistory rejects a resolved chain that repeats a hop", () => { const history = new AnnounceHistory(); history.start({ suffix: Path.from("a"), hops: [hop(1n), hop(2n)] }); diff --git a/js/net/src/lite/announce.ts b/js/net/src/lite/announce.ts index df2c18a5461..e621de20026 100644 --- a/js/net/src/lite/announce.ts +++ b/js/net/src/lite/announce.ts @@ -407,9 +407,8 @@ export class AnnounceHistory { const keep = msg.pathBase.keep; if (keep > head.length) throw new ProtocolViolation(`announce keeps ${keep} of ${head.length} segments`); suffix = Path.join(Path.from(...head.slice(0, keep)), suffix); - if (Path.parts(suffix).length > Path.MAX_PARTS) { - throw new ProtocolViolation(`path exceeds ${Path.MAX_PARTS} parts`); - } + const error = Path.check(suffix); + if (error) throw new ProtocolViolation(error); } const hops = this.#hops(msg.hops, msg.hopBase); this.#live.set(this.#next++, { suffix, hops }); diff --git a/js/net/src/lite/subscriber.test.ts b/js/net/src/lite/subscriber.test.ts index 3dc71585c9e..c0395a9b2e3 100644 --- a/js/net/src/lite/subscriber.test.ts +++ b/js/net/src/lite/subscriber.test.ts @@ -806,6 +806,31 @@ test("a violation on a very long path still ends the session", async () => { expect(new TextEncoder().encode(info?.reason ?? "").byteLength).toBeLessThanOrEqual(1024); }); +test.each([Version.DRAFT_02, Version.DRAFT_06])( + "version %p refuses a suffix that only the interest prefix pushes past the path limit", + async (version) => { + const { subscriber, send, sessionEnded, settle } = announceHarness(version); + // Each half fits on the wire alone; the joined path is one byte over. + const half = Path.from("x".repeat(Path.MAX_BYTES / 2)); + const announced = subscriber.announced(Path.Pattern.subtree(half)); + await settle(); + + if (version === Version.DRAFT_02) { + await send((w) => new AnnounceInit([half]).encode(w, version)); + } else { + await send((w) => new AnnounceOk(PEER, 0).encode(w, version)); + const active: AnnounceBroadcast = { status: "active", suffix: half, hops: [PUBLISHER_A] }; + await send((w) => encodeAnnounceBroadcast(w, active, version)); + } + + await expect(announced.next()).rejects.toThrow(`path exceeds ${Path.MAX_BYTES} bytes`); + expect((await sessionEnded())?.closeCode).toBe(SessionCode.ProtocolViolation); + + announced.close(); + subscriber.close(); + }, +); + test("a draft-04 duplicate start is a violation, not an update", async () => { // Pre-lite-05 has no replacement idiom, so a second ANNOUNCE_START for a live path is // the same violation lite-06 treats it as. Only lite-05, where the duplicate *is* the diff --git a/js/net/src/lite/subscriber.ts b/js/net/src/lite/subscriber.ts index c8926bbd94a..c0aed54db74 100644 --- a/js/net/src/lite/subscriber.ts +++ b/js/net/src/lite/subscriber.ts @@ -368,6 +368,8 @@ export class Subscriber { // ANNOUNCE_OK, so nothing names the publisher. for (const suffix of init.suffixes) { const path = Path.join(prefix, suffix); + const error = Path.check(path); + if (error) throw new ProtocolViolation(error); if (advertised.has(path)) { throw new ProtocolViolation(`duplicate announce for ${path}`); } @@ -494,6 +496,11 @@ export class Subscriber { continue; } + // Each suffix fits the wire limits alone, but the interest prefix can push the + // joined path past them, which the draft makes a violation too. + const error = Path.check(path); + if (error) throw new ProtocolViolation(error); + // One current advertisement per path per stream, decided before anything below // can skip this announcement. A second ANNOUNCE_START for a path the peer // already advertised is a violation whether or not its route would be usable diff --git a/js/net/src/path.test.ts b/js/net/src/path.test.ts index 82189ee22ab..1e9f8e4dc48 100644 --- a/js/net/src/path.test.ts +++ b/js/net/src/path.test.ts @@ -305,10 +305,10 @@ test("decode enforces the max part count", () => { }); test("decode never allocates per slash of a hostile path", () => { - // A peer can send a path as large as a control message; splitting it would hold one entry per - // slash, so decode must count separators and stop at the limit instead. - const slashes = "/".repeat(1024 * 1024); - const parts = "a/".repeat(1024 * 1024); + // Splitting a path would hold one entry per slash, so decode must count separators and stop at + // the limit instead, even for the most slashes MAX_BYTES admits. + const slashes = "/".repeat(Path.MAX_BYTES); + const parts = "a/".repeat(Path.MAX_BYTES / 2); const split = spyOn(String.prototype, "split"); const indexOf = spyOn(String.prototype, "indexOf"); try { @@ -336,3 +336,19 @@ test("encode enforces the max part count", () => { expect(Path.encode(atLimit)).toBe(atLimit); expect(() => Path.encode(`${atLimit}/extra` as Path.Valid)).toThrow(); }); + +test("encode and decode enforce the max byte length in UTF-8", () => { + const atLimit = "a".repeat(Path.MAX_BYTES) as Path.Valid; + expect(Path.encode(atLimit)).toBe(atLimit); + expect(Path.decode(atLimit)).toBe(atLimit); + expect(() => Path.encode(`${atLimit}b` as Path.Valid)).toThrow(); + expect(() => Path.decode(`${atLimit}b`)).toThrow(); + // Measured before normalization, so slashes can't pad a short path past the limit. + expect(() => Path.decode(`${"/".repeat(Path.MAX_BYTES)}a`)).toThrow(); + + // One UTF-16 unit of "é" is two UTF-8 bytes, so half as many units reach the limit. + const wide = "é".repeat(Math.floor(Path.MAX_BYTES / 2)) as Path.Valid; + expect(Path.encode(wide)).toBe(wide); + expect(() => Path.encode(`${wide}é` as Path.Valid)).toThrow(); + expect(() => Path.decode(`${wide}é`)).toThrow(); +}); diff --git a/js/net/src/path.ts b/js/net/src/path.ts index e79ac672a52..5564a8bb8e2 100644 --- a/js/net/src/path.ts +++ b/js/net/src/path.ts @@ -56,6 +56,11 @@ export type Relative = string & { __brand: "Relative" }; */ export const MAX_PARTS = 32; +/** Maximum UTF-8 length of a path in bytes, leaving room in a 64 KiB moq-lite message for its other fields. */ +export const MAX_BYTES = 16384; + +const utf8 = new TextEncoder(); + /** Build a path from one or more components, joining with "/" and trimming redundant slashes. */ export function from(...paths: string[]): Valid { // Join paths with "/" and then remove leading and trailing slashes, and collapse multiple slashes into one. @@ -69,30 +74,44 @@ export function parts(path: Valid): string[] { } /** - * Validate an untrusted wire string as a path, enforcing {@link MAX_PARTS}. + * Validate an untrusted wire string as a path, enforcing {@link MAX_PARTS} and {@link MAX_BYTES}. * - * Throws when the path has too many parts; use at wire decode sites. + * Throws when the path is too deep or too long; use at wire decode sites. */ export function decode(raw: string): Valid { + // Measured before normalization, so redundant slashes can't smuggle a longer string. + // A UTF-16 unit is at least one UTF-8 byte, so a long string fails without encoding it. + if (raw.length > MAX_BYTES || utf8.encode(raw).length > MAX_BYTES) { + throw new Error(`path is longer than ${MAX_BYTES} bytes`); + } const path = from(raw); return encode(path); } /** - * Validate a path before writing it to the wire, enforcing {@link MAX_PARTS}. + * Validate a path before writing it to the wire, enforcing {@link MAX_PARTS} and {@link MAX_BYTES}. * - * Throws when the path has too many parts; use at wire encode sites so we never + * Throws when the path is too deep or too long; use at wire encode sites so we never * emit a path the remote side is required to reject. */ export function encode(path: Valid): Valid { + const error = check(path); + if (error) throw new Error(error); + return path; +} + +/** Why `path` breaks the wire limits, or undefined when it fits. @internal */ +export function check(path: Valid): string | undefined { // Count separators with an early exit instead of splitting, so a hostile path never allocates per part. let count = path === "" ? 0 : 1; for (let i = path.indexOf("/"); i !== -1; i = path.indexOf("/", i + 1)) { - if (++count > MAX_PARTS) { - throw new Error(`path exceeds ${MAX_PARTS} parts`); - } + if (++count > MAX_PARTS) return `path exceeds ${MAX_PARTS} parts`; } - return path; + // A UTF-16 unit is at most 3 UTF-8 bytes, so only a long path needs encoding to measure. + if (path.length > MAX_BYTES / 3 && utf8.encode(path).length > MAX_BYTES) { + return `path exceeds ${MAX_BYTES} bytes`; + } + return undefined; } /** diff --git a/quest/m2/audit-compile-time.md b/quest/m2/audit-compile-time.md index 67d51792e5f..8835f442463 100644 --- a/quest/m2/audit-compile-time.md +++ b/quest/m2/audit-compile-time.md @@ -107,7 +107,7 @@ instantiating the whole map. in clean builds; the crypto provider stays (m0). usage-rs derive output scales with config fields and is external code. serde visitors per deserializer are inherent to TOML config. moq-rtmp, moq-srt and moq-rtc - entry points generic over `impl AsPath` instantiate their async bodies in + entry points generic over `impl Into` instantiate their async bodies in the `moq` bin (20,327 lines with drop glue, 1.8%); a non-generic inner function would move them, but adds code for little. The catalog JSON diff instantiated per crate is [catalog publish once](/quest/m2/catalog-publish-once.md). diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md index 6d8b78877a0..255bb4491b0 100644 --- a/rs/hang/MIGRATE.md +++ b/rs/hang/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `moq_net::Path` has no lifetime + +`catalog` reference fields, `Echo::path`, and `health::Observer` use the lifetime-free `moq_net::path::Relative` and `moq_net::Path`: replace `RelativeOwned` with `Relative` and `PathOwned` with `Path`. `Echo::new` takes `impl Into`: pass `Relative::new(s)?` rather than a string. See moq-net's migration for the rest. + ### The catalog rendition cap is removed `catalog::MAX_RENDITIONS` and `Error::TooManyRenditions` are gone: a catalog with any number of renditions is accepted. diff --git a/rs/hang/src/catalog/archive.rs b/rs/hang/src/catalog/archive.rs index c988e6b007b..affa595fa75 100644 --- a/rs/hang/src/catalog/archive.rs +++ b/rs/hang/src/catalog/archive.rs @@ -52,7 +52,7 @@ pub struct Archive { /// The MoQ broadcast the archive is served back from, relative to this catalog, if any. #[serde(default)] - pub replay: Option, + pub replay: Option, /// The publisher instance [`replay`](Self::replay) names, when known. Absent accepts the /// first one a reader resolves; see [`Reference::epoch`](super::Reference::epoch). @@ -122,7 +122,7 @@ mod test { #[test] fn recording_roundtrips_replay_store_and_version() { let mut archive = live(); - archive.replay = Some(moq_net::path::RelativeOwned::new("./recordings/clip")); + archive.replay = Some(moq_net::path::Relative::new("./recordings/clip").unwrap()); archive.store = Some("https://objects.example/rec/".parse().unwrap()); archive.version = Some(Archive::VERSION); diff --git a/rs/hang/src/catalog/audio/mod.rs b/rs/hang/src/catalog/audio/mod.rs index cfdb3d214f3..73f1ad250dc 100644 --- a/rs/hang/src/catalog/audio/mod.rs +++ b/rs/hang/src/catalog/audio/mod.rs @@ -101,7 +101,7 @@ pub struct AudioConfig { /// Resolve it with [`Path::resolve`](moq_net::Path::resolve): a reference that walks /// above the root names no broadcast, so the catalog is rejected. #[serde(default)] - pub broadcast: Option, + pub broadcast: Option, /// The track's name in its broadcast, when it differs from this entry's ID: its key in the /// catalog. Absent means the ID is the track name, as in every catalog before this field. diff --git a/rs/hang/src/catalog/binary.rs b/rs/hang/src/catalog/binary.rs index f0b08dab552..7d24416fa39 100644 --- a/rs/hang/src/catalog/binary.rs +++ b/rs/hang/src/catalog/binary.rs @@ -71,7 +71,7 @@ pub struct BinaryConfig { /// broadcast that served this catalog (e.g. `./source`). If unset, the track lives in the same /// broadcast as the catalog. #[serde(default)] - pub broadcast: Option, + pub broadcast: Option, /// The track's name in its broadcast, when it differs from this entry's ID: its key in the /// catalog. Absent means the ID is the track name, as in every catalog before this field. diff --git a/rs/hang/src/catalog/consumer.rs b/rs/hang/src/catalog/consumer.rs index 114c75b1080..216e62f2679 100644 --- a/rs/hang/src/catalog/consumer.rs +++ b/rs/hang/src/catalog/consumer.rs @@ -2,7 +2,7 @@ use std::task::{Poll, ready}; -use moq_net::PathOwned; +use moq_net::Path; use serde::de::DeserializeOwned; use super::Catalog; @@ -12,7 +12,7 @@ use crate::Result; pub struct Consumer { // Decoded in two steps, so a catalog needing a newer reader fails with a typed error. inner: moq_json::snapshot::Consumer, - base: PathOwned, + base: Path, _ext: std::marker::PhantomData, } @@ -75,7 +75,7 @@ impl Consumer { /// /// Every section carrying a relative broadcast path must be included here; a section left out /// silently exempts its paths from the containment check. -fn check_resolvable(base: &PathOwned, catalog: &Catalog) -> Result<()> { +fn check_resolvable(base: &Path, catalog: &Catalog) -> Result<()> { let video = catalog.video.renditions.iter(); let audio = catalog.audio.renditions.iter(); let text = catalog.text.renditions.iter(); @@ -110,13 +110,13 @@ mod test { #[test] fn echo_prefix_cannot_escape_the_catalog_root() { - let base = moq_net::PathOwned::from("room/live"); + let base = moq_net::Path::from_static("room/live"); let mut catalog = Catalog::<()>::from_str(include_str!("../../fixtures/catalog-stats-echo.json")).unwrap(); assert!(check_resolvable(&base, &catalog).is_ok()); let echo = catalog.echo.as_mut().unwrap(); - echo.path = moq_net::path::RelativeOwned::from("../viewers"); + echo.path = moq_net::path::Relative::new("../viewers").unwrap(); assert!(check_resolvable(&base, &catalog).is_ok()); - catalog.echo.as_mut().unwrap().path = moq_net::path::RelativeOwned::from("../../viewers"); + catalog.echo.as_mut().unwrap().path = moq_net::path::Relative::new("../../viewers").unwrap(); assert!(matches!( check_resolvable(&base, &catalog), Err(crate::Error::EscapingBroadcast(_)) @@ -127,7 +127,7 @@ mod test { async fn subscribe_validates_echo_prefix_before_yielding() { for (reference, accepted) in [("viewers", true), ("../../viewers", false)] { let mut info = moq_net::broadcast::Info::new(); - info.path = "room/live".into(); + info.path = moq_net::Path::from_static("room/live"); let broadcast = info.produce(); let track = broadcast .create_track(Catalog::DEFAULT_NAME, Catalog::default_track_info()) diff --git a/rs/hang/src/catalog/echo.rs b/rs/hang/src/catalog/echo.rs index a56863c40a9..a1a7e87724f 100644 --- a/rs/hang/src/catalog/echo.rs +++ b/rs/hang/src/catalog/echo.rs @@ -5,12 +5,12 @@ use serde::{Deserialize, Serialize}; #[non_exhaustive] pub struct Echo { /// The feedback prefix relative to the broadcast serving this catalog. - pub path: moq_net::path::RelativeOwned, + pub path: moq_net::path::Relative, } impl Echo { /// Solicit feedback under a prefix relative to the catalog's broadcast. - pub fn new(path: impl Into) -> Self { + pub fn new(path: impl Into) -> Self { Self { path: path.into() } } } diff --git a/rs/hang/src/catalog/json.rs b/rs/hang/src/catalog/json.rs index 10478082256..48b0307464b 100644 --- a/rs/hang/src/catalog/json.rs +++ b/rs/hang/src/catalog/json.rs @@ -69,7 +69,7 @@ pub struct JsonConfig { /// broadcast that served this catalog (e.g. `./source`). If unset, the track lives in the same /// broadcast as the catalog. #[serde(default)] - pub broadcast: Option, + pub broadcast: Option, /// The track's name in its broadcast, when it differs from this entry's ID: its key in the /// catalog. Absent means the ID is the track name, as in every catalog before this field. diff --git a/rs/hang/src/catalog/reference.rs b/rs/hang/src/catalog/reference.rs index 40f982854c6..2647c1ee3d5 100644 --- a/rs/hang/src/catalog/reference.rs +++ b/rs/hang/src/catalog/reference.rs @@ -8,7 +8,7 @@ use super::{AudioConfig, BinaryConfig, JsonConfig, TextConfig, VideoConfig}; /// optionally names the publisher instance the broadcast must be. pub trait Reference { /// The broadcast publishing the track, relative to the catalog's; `None` is the catalog's own. - fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned>; + fn broadcast(&self) -> Option<&moq_net::path::Relative>; /// The publisher instance the broadcast must be; `None` accepts the first one resolved. fn epoch(&self) -> Option<&moq_net::Epoch>; @@ -20,7 +20,7 @@ pub trait Reference { macro_rules! reference { ($($config:ty),*) => {$( impl Reference for $config { - fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> { + fn broadcast(&self) -> Option<&moq_net::path::Relative> { self.broadcast.as_ref() } diff --git a/rs/hang/src/catalog/root.rs b/rs/hang/src/catalog/root.rs index 48a5ff69ccd..b6543cef841 100644 --- a/rs/hang/src/catalog/root.rs +++ b/rs/hang/src/catalog/root.rs @@ -1020,7 +1020,12 @@ mod test { } let catalog = Catalog::<()>::from_str(r#"{"stats":{"track":"stats.json"},"echo":{"path":"viewers"}}"#).unwrap(); assert_eq!(catalog.stats, Some(crate::catalog::Stats::new("stats.json"))); - assert_eq!(catalog.echo, Some(crate::catalog::Echo::new("viewers"))); + assert_eq!( + catalog.echo, + Some(crate::catalog::Echo::new( + moq_net::path::Relative::new("viewers").unwrap() + )) + ); } #[test] @@ -1096,7 +1101,7 @@ mod test { "video".to_string(), crate::catalog::Timeline::new("video.timeline.z", 2_000), ); - archive.replay = Some(moq_net::path::RelativeOwned::new("recordings/clip")); + archive.replay = Some(moq_net::path::Relative::new("recordings/clip").unwrap()); archive.version = Some(crate::catalog::Archive::VERSION); let catalog = Catalog::<()> { @@ -1192,7 +1197,7 @@ mod test { chat.schema = Some("https://example.com/chat.schema.json".to_string()); let mut status = JsonConfig::new(Mode::Snapshot); - status.broadcast = Some(moq_net::path::RelativeOwned::new("source")); + status.broadcast = Some(moq_net::path::Relative::new("source").unwrap()); let mut thumbnail = BinaryConfig::new(Mode::Snapshot); thumbnail.mime = Some("image/jpeg".to_string()); diff --git a/rs/hang/src/catalog/text/mod.rs b/rs/hang/src/catalog/text/mod.rs index fb0de18a2be..a0b7b49b34d 100644 --- a/rs/hang/src/catalog/text/mod.rs +++ b/rs/hang/src/catalog/text/mod.rs @@ -77,7 +77,7 @@ pub struct TextConfig { /// broadcast that served this catalog (e.g. `../source`). If unset, the track lives in the same /// broadcast as the catalog. #[serde(default)] - pub broadcast: Option, + pub broadcast: Option, /// The track's name in its broadcast, when it differs from this entry's ID: its key in the /// catalog. Absent means the ID is the track name, as in every catalog before this field. diff --git a/rs/hang/src/catalog/video/mod.rs b/rs/hang/src/catalog/video/mod.rs index 3de3607c62a..c894813bebe 100644 --- a/rs/hang/src/catalog/video/mod.rs +++ b/rs/hang/src/catalog/video/mod.rs @@ -188,7 +188,7 @@ pub struct VideoConfig { /// Resolve it with [`Path::resolve`](moq_net::Path::resolve): a reference that walks /// above the root names no broadcast, so the catalog is rejected. #[serde(default)] - pub broadcast: Option, + pub broadcast: Option, /// The track's name in its broadcast, when it differs from this entry's ID: its key in the /// catalog. Absent means the ID is the track name, as in every catalog before this field. diff --git a/rs/hang/src/health.rs b/rs/hang/src/health.rs index 9310eb77006..57a9ad14ffd 100644 --- a/rs/hang/src/health.rs +++ b/rs/hang/src/health.rs @@ -40,7 +40,7 @@ pub enum Role { #[non_exhaustive] pub struct Observer { /// The publisher's media broadcast, or the viewer's echo broadcast. - pub path: moq_net::PathOwned, + pub path: moq_net::Path, /// The reporting broadcast's publisher epoch, when its route carries one. #[serde_as(as = "Option")] #[serde(skip_serializing_if = "Option::is_none")] @@ -49,7 +49,7 @@ pub struct Observer { impl Observer { /// Name the broadcast a report was read from. - pub fn new(path: moq_net::PathOwned, epoch: Option) -> Self { + pub fn new(path: moq_net::Path, epoch: Option) -> Self { Self { path, epoch } } } @@ -642,7 +642,7 @@ mod tests { fn report(fixture: &Fixture) -> Report { let epoch = fixture.observer.epoch.as_deref().map(|epoch| epoch.parse().unwrap()); - let observer = Observer::new(moq_net::Path::new(&fixture.observer.path).to_owned(), epoch); + let observer = Observer::new(moq_net::Path::new(&fixture.observer.path).unwrap(), epoch); Report::new(observer, serde_json::from_value(fixture.snapshot.clone()).unwrap()) } @@ -721,7 +721,7 @@ mod tests { ..Default::default() } }; - let observer = Observer::new(moq_net::Path::new("room/live").to_owned(), None); + let observer = Observer::new(moq_net::Path::from_static("room/live"), None); let previous = Report::new(observer.clone(), snapshot(0, 0)); let current = Report::new(observer, snapshot(u64::MAX, u64::MAX)); let sample = Sample::new(¤t, Duration::ZERO).with_previous(&previous, Duration::from_secs(1)); diff --git a/rs/hang/src/snapshot.rs b/rs/hang/src/snapshot.rs index a2806d88c18..9f9ba99637f 100644 --- a/rs/hang/src/snapshot.rs +++ b/rs/hang/src/snapshot.rs @@ -200,9 +200,9 @@ mod tests { #[test] fn echo_suffix_filters_only_the_broadcast_name() { - assert!(echo::is_echo(&moq_net::Path::new("room/alice.echo"))); - assert!(!echo::is_echo(&moq_net::Path::new("room.echo/alice"))); - assert!(!echo::is_echo(&moq_net::Path::new("room/alice.echoes"))); + assert!(echo::is_echo(&moq_net::Path::from_static("room/alice.echo"))); + assert!(!echo::is_echo(&moq_net::Path::from_static("room.echo/alice"))); + assert!(!echo::is_echo(&moq_net::Path::from_static("room/alice.echoes"))); } #[test] diff --git a/rs/moq-c/src/consume.rs b/rs/moq-c/src/consume.rs index b54ff3f6cc6..e151f2736c1 100644 --- a/rs/moq-c/src/consume.rs +++ b/rs/moq-c/src/consume.rs @@ -116,7 +116,7 @@ struct ConsumeBroadcast { async fn resolve( broadcast: moq_net::broadcast::Consumer, origin: Option, - reference: Option, + reference: Option, ) -> Result { let Some(reference) = reference.filter(|reference| !reference.is_empty()) else { return Ok(broadcast); @@ -128,7 +128,7 @@ async fn resolve( // first instance resolved, without the epoch's guarantee. origin .driver - .watch(async { Ok(source.resolve(Some(&reference.borrow()), None).await?) }) + .watch(async { Ok(source.resolve(Some(&reference), None).await?) }) .await } diff --git a/rs/moq-c/src/origin.rs b/rs/moq-c/src/origin.rs index 8daea7d5fc7..e2f8fe5354c 100644 --- a/rs/moq-c/src/origin.rs +++ b/rs/moq-c/src/origin.rs @@ -321,7 +321,7 @@ impl Origin { /// Create an unannounced broadcast at `path` on an origin. /// /// Errors with [`Error::Moq`] if the path is outside the origin's scope. - pub fn create_broadcast( + pub fn create_broadcast>>( &self, origin: Id, path: P, diff --git a/rs/moq-c/src/test.rs b/rs/moq-c/src/test.rs index fd40b354646..48281728bb3 100644 --- a/rs/moq-c/src/test.rs +++ b/rs/moq-c/src/test.rs @@ -3729,7 +3729,7 @@ fn consume_audio_follows_a_sibling_broadcast_reference() { .renditions .get_mut(&name) .unwrap() - .broadcast = Some(moq_net::path::Relative::new("./source").into_owned()); + .broadcast = Some(moq_net::path::Relative::new("./source").unwrap()); } let consume = request_broadcast(origin, b"a/pub"); diff --git a/rs/moq-cli/src/archive.rs b/rs/moq-cli/src/archive.rs index 2946db80704..d83a7ef08d3 100644 --- a/rs/moq-cli/src/archive.rs +++ b/rs/moq-cli/src/archive.rs @@ -47,7 +47,7 @@ pub struct ImportArgs { } /// One publisher instance being recorded: its path, and its epoch when its route has one. -type Instance = (moq_net::PathOwned, Option); +type Instance = (moq_net::Path, Option); /// Record every broadcast announced at or under `prefix` into `args.store`, each publisher /// instance under `/@`, until the announcements end. @@ -68,7 +68,7 @@ pub async fn export(origin: moq_net::origin::Consumer, prefix: String, args: Exp // Announcements leave hidden routes out unless asked, and a prefix naming one asks. let hidden = prefix.split('/').any(|segment| segment.starts_with('.')); - let prefix = moq_net::Path::new(&prefix).to_owned(); + let prefix = moq_net::Path::new(&prefix)?; let mut announced = origin.clone().with_hidden(hidden).announced(); let mut recordings = tokio::task::JoinSet::new(); let mut running = Running::default(); @@ -115,11 +115,11 @@ pub async fn export(origin: moq_net::origin::Consumer, prefix: String, args: Exp /// The broadcasts being recorded. Announcements read after another instance took over all /// resolve that one, so it is recorded once. #[derive(Clone, Default)] -struct Claims(Arc>>>); +struct Claims(Arc>>>); impl Claims { /// Claim `broadcast` for recording, or `None` when it is recorded already. - fn claim(&self, path: &moq_net::PathOwned, broadcast: &moq_net::broadcast::Consumer) -> Option { + fn claim(&self, path: &moq_net::Path, broadcast: &moq_net::broadcast::Consumer) -> Option { let mut claims = self.0.lock().unwrap(); let claimed = claims.entry(path.clone()).or_default(); if claimed.iter().any(|other| other.is_clone(broadcast)) { @@ -137,7 +137,7 @@ impl Claims { /// A broadcast being recorded, released on drop. struct Claim { claims: Claims, - path: moq_net::PathOwned, + path: moq_net::Path, broadcast: moq_net::broadcast::Consumer, } @@ -442,12 +442,12 @@ mod tests { #[test] fn a_re_announced_instance_is_recorded() { let mut running = Running::default(); - let epochless = (moq_net::Path::new("live/a").to_owned(), None); + let epochless = (moq_net::Path::from_static("live/a"), None); assert!(running.start(&epochless)); assert!(running.start(&epochless)); assert!(!running.done(&epochless)); - let pinned = (moq_net::Path::new("live/b").to_owned(), Some(moq_net::Epoch::mint())); + let pinned = (moq_net::Path::from_static("live/b"), Some(moq_net::Epoch::mint())); assert!(running.start(&pinned)); assert!(!running.start(&pinned)); assert!(running.done(&pinned)); @@ -460,7 +460,7 @@ mod tests { #[tokio::test] async fn a_resolved_broadcast_is_recorded_once() { let origin = moq_tokio::origin::spawn(); - let path = moq_net::Path::new("live/a").to_owned(); + let path = moq_net::Path::from_static("live/a"); let newest = origin.create_broadcast(&path).unwrap(); let older = origin.create_broadcast(&path).unwrap(); @@ -479,14 +479,14 @@ mod tests { /// share a key prefix. #[test] fn an_epochless_instance_gets_a_fresh_key() { - let instance = (moq_net::Path::new("live/a").to_owned(), None); + let instance = (moq_net::Path::from_static("live/a"), None); let first = key(&Path::from("rec"), &instance).unwrap(); let second = key(&Path::from("rec"), &instance).unwrap(); assert_ne!(first, second); assert!(first.as_ref().starts_with("rec/live/a/"), "{first}"); let epoch = moq_net::Epoch::mint(); - let pinned = (moq_net::Path::new("live/a").to_owned(), Some(epoch.clone())); + let pinned = (moq_net::Path::from_static("live/a"), Some(epoch.clone())); assert_eq!( key(&Path::from("rec"), &pinned).unwrap().as_ref(), format!("rec/live/a/@{epoch}") @@ -500,13 +500,13 @@ mod tests { let epoch = moq_net::Epoch::mint(); let victim = key( &Path::from("rec"), - &(moq_net::Path::new("live/a").to_owned(), Some(epoch.clone())), + &(moq_net::Path::from_static("live/a"), Some(epoch.clone())), ) .unwrap(); let hostile = format!("live/a/@{epoch}"); let nested = key( &Path::from("rec"), - &(moq_net::Path::new(&hostile).to_owned(), Some(moq_net::Epoch::mint())), + &(moq_net::Path::new(&hostile).unwrap(), Some(moq_net::Epoch::mint())), ) .unwrap(); assert!(nested.prefix_match(&victim).is_none(), "{nested} is under {victim}"); @@ -518,10 +518,7 @@ mod tests { // `%` is escaped too, so an escaped-looking segment keys apart from the `@` one. let literal = key( &Path::from("rec"), - &( - moq_net::Path::new(&format!("live/a/%40{epoch}")).to_owned(), - Some(epoch), - ), + &(moq_net::Path::new(&format!("live/a/%40{epoch}")).unwrap(), Some(epoch)), ) .unwrap(); assert!(literal.as_ref().starts_with("rec/live/a/%2540"), "{literal}"); diff --git a/rs/moq-cli/src/args.rs b/rs/moq-cli/src/args.rs index b850ad2c92f..c58e2b6a519 100644 --- a/rs/moq-cli/src/args.rs +++ b/rs/moq-cli/src/args.rs @@ -338,14 +338,20 @@ impl Invocation { } // A reader refuses the whole catalog over a path it cannot resolve. let broadcast = command.broadcast(&self.moq); - let rel = hang::moq_net::path::Relative::new(path); - let Some(prefix) = hang::moq_net::Path::new(&broadcast).try_resolve(&rel) else { + let rel = hang::moq_net::path::Relative::new(path) + .map_err(|err| anyhow::anyhow!("invalid --echo-path: {err}"))?; + let broadcast = hang::moq_net::Path::new(&broadcast) + .map_err(|err| anyhow::anyhow!("invalid --broadcast: {err}"))?; + let Some(prefix) = broadcast.try_resolve(&rel) else { anyhow::bail!("--echo-path `{path}` walks above the connection root from broadcast `{broadcast}`"); }; // An origin refuses a broadcast a pattern could not name, so no viewer could // publish its `.echo` beneath this. - let feedback = prefix.join("viewer.echo"); - if let Err(err) = hang::moq_net::Pattern::literal(feedback.as_str()) { + let feedback = prefix.join(&hang::moq_net::Path::from_static("viewer.echo")); + let literal = feedback + .map_err(anyhow::Error::from) + .and_then(|feedback| Ok(hang::moq_net::Pattern::literal(feedback.as_str())?)); + if let Err(err) = literal { anyhow::bail!( "--echo-path `{path}` resolves to `{prefix}`, where no viewer can publish its feedback: {err}" ); diff --git a/rs/moq-cli/src/main.rs b/rs/moq-cli/src/main.rs index 7018a403f5c..eda6fb07fbb 100644 --- a/rs/moq-cli/src/main.rs +++ b/rs/moq-cli/src/main.rs @@ -586,7 +586,12 @@ async fn run_stages(moq: MoqSide, stages: Vec, net: Net) -> anyhow::Res claim("stdin", &mut stdin, &name)?; } let report = import.report; - let echo = import.echo_path.clone().map(hang::catalog::Echo::new); + let echo = import + .echo_path + .as_deref() + .map(hang::moq_net::path::Relative::new) + .transpose()? + .map(hang::catalog::Echo::new); if let Some(mut publish) = spawn_import(&origin, import, name, epoch.clone(), bandwidth.clone(), &mut tasks)? { @@ -744,7 +749,7 @@ fn spawn_import( }) = &import.source { let name = require_broadcast(name, "import ts --program all")?; - Publish::ts_programs(origin.clone(), name, config) + Publish::ts_programs(origin.clone(), moq_net::Path::new(&name)?, config) } else { let broadcast = origin.create_broadcast(&name).context("failed to create broadcast")?; Publish::from_publication(broadcast, &format, config)? diff --git a/rs/moq-cli/src/play/media.rs b/rs/moq-cli/src/play/media.rs index 91c856b5621..faaa17e7f82 100644 --- a/rs/moq-cli/src/play/media.rs +++ b/rs/moq-cli/src/play/media.rs @@ -1858,7 +1858,7 @@ mod tests { let source = moq_mux::Source::new(origin.consume(), broadcast.consume()); let mut config = AudioConfig::new(AudioCodec::Pcm, SAMPLE_RATE, 1); let absent = Track::of(&source, &config, "audio"); - config.broadcast = Some(moq_net::path::RelativeOwned::new("")); + config.broadcast = Some(moq_net::path::Relative::new("").unwrap()); assert_eq!(Track::of(&source, &config, "audio"), absent); } diff --git a/rs/moq-cli/src/play/playback.rs b/rs/moq-cli/src/play/playback.rs index 6bed70c3bfb..4fea9011e66 100644 --- a/rs/moq-cli/src/play/playback.rs +++ b/rs/moq-cli/src/play/playback.rs @@ -17,7 +17,7 @@ pub(super) enum Kind { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(super) struct Track { /// Resolved rather than as written, since several references name one broadcast. - broadcast: Option, + broadcast: Option, name: String, } diff --git a/rs/moq-cli/src/publish.rs b/rs/moq-cli/src/publish.rs index 7c2eb061a04..230e64e0ac0 100644 --- a/rs/moq-cli/src/publish.rs +++ b/rs/moq-cli/src/publish.rs @@ -445,7 +445,11 @@ impl Publish { /// The programs aren't known until the PAT arrives, so each broadcast is created then and /// announced once its catalog has named a servable track, and [`announce`](Self::announce) has /// nothing to do. - pub fn ts_programs(origin: moq_net::origin::Producer, name: String, config: moq_mux::catalog::Config) -> Self { + pub fn ts_programs( + origin: moq_net::origin::Producer, + name: moq_net::Path, + config: moq_mux::catalog::Config, + ) -> Self { Self { source: Source::Stream { decoder: PublishDecoder::TsPrograms(Box::new(ts::Programs::new(origin, name, config))), @@ -1512,7 +1516,7 @@ pub(crate) mod tests { async fn echo_rides_the_first_catalog() { let broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let echo = hang::catalog::Echo::new("viewers".to_string()); + let echo = hang::catalog::Echo::new(moq_net::path::Relative::new("viewers").unwrap()); let mut publish = Publish::new(broadcast, &PublishFormat::Ts { program: None }, Default::default()) .unwrap() .with_echo(echo.clone()) diff --git a/rs/moq-cli/src/report.rs b/rs/moq-cli/src/report.rs index 039a6e3749b..d05a2b41542 100644 --- a/rs/moq-cli/src/report.rs +++ b/rs/moq-cli/src/report.rs @@ -263,7 +263,7 @@ pub struct EchoTarget { /// The feedback broadcast in force: where it is published and its snapshot tracks. struct Feedback { - path: moq_net::PathOwned, + path: moq_net::Path, // Held for the route: dropping it retracts the announcement. _broadcast: moq_net::origin::Publication, tracks: Pair>, @@ -334,7 +334,12 @@ impl Echo { if prefix.is_none() { tracing::warn!(path = %echo.path.as_str(), "the catalog solicits feedback above the connection root"); } - Some(prefix?.join(format!("{}{}", self.target.name.0, hang::echo::SUFFIX).as_str())) + let prefix = prefix?; + let name = format!("{}{}", self.target.name.0, hang::echo::SUFFIX); + hang::moq_net::Path::new(&name) + .and_then(|name| prefix.join(&name)) + .inspect_err(|err| tracing::warn!(%err, path = %echo.path.as_str(), "the feedback path is too long")) + .ok() }); if self.feedback.as_ref().map(|feedback| &feedback.path) == target.as_ref() { return; @@ -353,7 +358,7 @@ impl Echo { } } - fn publish(&self, path: &moq_net::PathOwned) -> anyhow::Result { + fn publish(&self, path: &moq_net::Path) -> anyhow::Result { let broadcast = self.target.origin.create_broadcast(path)?; let tracks = Pair::create(&mut broadcast.broadcast().clone(), hang::echo::TRACK)?; // A fresh instance, so a reader sees a replaced feedback broadcast as a restart. @@ -488,7 +493,7 @@ async fn write_echo( ) -> anyhow::Result<()> { // Announcements leave hidden routes out unless asked, and a prefix naming one asks. let hidden = prefix.split('/').any(|segment| segment.starts_with('.')); - let prefix = moq_net::Path::new(&prefix).to_owned(); + let prefix = moq_net::Path::new(&prefix)?; let mut announced = origin.clone().with_hidden(hidden).announced(); let (lines, mut written) = tokio::sync::mpsc::channel::(LINES); let mut readers = Readers::default(); @@ -527,14 +532,14 @@ async fn write_echo( /// The `export echo` reader of each feedback broadcast, live or draining after its `End`. #[derive(Default)] struct Readers { - running: HashMap>, + running: HashMap>, /// One timer per `End`, yielding the path and the reader it ended once `DRAIN` passes. - drains: tokio::task::JoinSet<(moq_net::PathOwned, tokio::task::Id)>, + drains: tokio::task::JoinSet<(moq_net::Path, tokio::task::Id)>, } impl Readers { /// Read `path` with `reader`, replacing any reader still on it. - fn start(&mut self, path: moq_net::PathOwned, reader: tokio::task::JoinHandle<()>) { + fn start(&mut self, path: moq_net::Path, reader: tokio::task::JoinHandle<()>) { if let Some(previous) = self.running.insert(path, reader) { previous.abort(); } @@ -542,7 +547,7 @@ impl Readers { /// Let the reader of `path` drain, not abort it: the final snapshot can still be in flight. It /// stays so a start at the path replaces it, and goes after `DRAIN` even if its track stays open. - fn end(&mut self, path: moq_net::PathOwned) { + fn end(&mut self, path: moq_net::Path) { let Some(reader) = self.running.get(&path) else { return; }; @@ -573,11 +578,7 @@ impl Readers { } /// Send a line for each feedback snapshot the broadcast at `path` publishes, until it ends. -async fn read_echo( - origin: moq_net::origin::Consumer, - path: moq_net::PathOwned, - lines: tokio::sync::mpsc::Sender, -) { +async fn read_echo(origin: moq_net::origin::Consumer, path: moq_net::Path, lines: tokio::sync::mpsc::Sender) { let read = async { let broadcast = origin.request_broadcast(&path, None).await?; let subscriber = broadcast.track(hang::echo::TRACK)?.subscribe(None).await?; @@ -669,7 +670,7 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_drained_reader_is_forgotten() { let mut readers = Readers::default(); - let path = moq_net::Path::new("viewer.echo").to_owned(); + let path = moq_net::Path::from_static("viewer.echo"); readers.start(path.clone(), tokio::spawn(std::future::pending())); readers.end(path.clone()); diff --git a/rs/moq-cli/src/report/echo_test.rs b/rs/moq-cli/src/report/echo_test.rs index d07576b2eba..8a508b3c23d 100644 --- a/rs/moq-cli/src/report/echo_test.rs +++ b/rs/moq-cli/src/report/echo_test.rs @@ -19,7 +19,9 @@ async fn export_echo_reports_where_the_import_solicits() { let publication = origin.create_broadcast("demo").unwrap(); let mut broadcast = publication.broadcast().clone(); let mut catalog_value = moq_mux::catalog::hang::Catalog::::default(); - catalog_value.echo = Some(hang::catalog::Echo::new("viewers")); + catalog_value.echo = Some(hang::catalog::Echo::new( + hang::moq_net::path::Relative::new("viewers").unwrap(), + )); let config = moq_mux::catalog::Config::default() .with_catalog(catalog_value) .with_max_age(Duration::from_secs(1)); diff --git a/rs/moq-cli/src/rtmp.rs b/rs/moq-cli/src/rtmp.rs index 2a3dd43368f..8039444f956 100644 --- a/rs/moq-cli/src/rtmp.rs +++ b/rs/moq-cli/src/rtmp.rs @@ -73,6 +73,7 @@ pub async fn listen_import(target: ImportTarget, addr: SocketAddr) -> anyhow::Re linger, bandwidth, } = target; + let name = moq_net::Path::new(&name)?; let mut server = Server::bind(addr).await?; tracing::info!(%addr, %name, "RTMP listening (import)"); notify_ready(); @@ -114,6 +115,7 @@ pub async fn listen_export( name: String, max_delay: Duration, ) -> anyhow::Result<()> { + let name = moq_net::Path::new(&name)?; let mut server = Server::bind(addr).await?; tracing::info!(%addr, %name, "RTMP listening (export)"); notify_ready(); @@ -146,7 +148,7 @@ pub async fn listen_export( /// Dial a remote RTMP server and pull its play into the Origin under `target.name` (import). pub async fn connect_import(target: ImportTarget, url: Url) -> anyhow::Result<()> { let (addr, app, key) = parse_url(&url).await?; - let name = &target.name; + let name = moq_net::Path::new(&target.name)?; // The stream key is the ingest credential, so log the dial target and app instead. tracing::info!(%addr, %app, %name, "RTMP client pulling"); notify_ready(); @@ -166,6 +168,7 @@ pub async fn connect_export( name: String, max_delay: Duration, ) -> anyhow::Result<()> { + let name = moq_net::Path::new(&name)?; let (addr, app, key) = parse_url(&url).await?; // Confirm the broadcast is reachable (and wait for it to be announced) before dialing; // the FLV export re-resolves it (and any referenced sibling broadcast) through the origin. diff --git a/rs/moq-cli/src/srt.rs b/rs/moq-cli/src/srt.rs index a08f65e35d4..451a9bb3147 100644 --- a/rs/moq-cli/src/srt.rs +++ b/rs/moq-cli/src/srt.rs @@ -102,6 +102,7 @@ pub async fn listen_import( linger, bandwidth, } = target; + let name = moq_net::Path::new(&name)?; let mut server = Server::bind(addr, latency).await?; tracing::info!(%addr, %name, "SRT listening (import)"); notify_ready(); @@ -145,6 +146,7 @@ pub async fn listen_export( name: String, args: ExportArgs, ) -> anyhow::Result<()> { + let name = moq_net::Path::new(&name)?; let mut server = Server::bind(addr, Duration::from(args.endpoint.latency)).await?; let (follow, stitch) = (args.follow.into(), args.stitch); tracing::info!(%addr, %name, "SRT listening (export)"); @@ -182,7 +184,7 @@ pub async fn connect_import( program: Option, ) -> anyhow::Result<()> { let (addr, resource) = parse_url(&url).await?; - let name = &target.name; + let name = moq_net::Path::new(&target.name)?; tracing::info!(url = %RedactedUrl::new(&url), %name, "SRT client pulling"); notify_ready(); @@ -202,6 +204,7 @@ pub async fn connect_export( name: String, args: ExportArgs, ) -> anyhow::Result<()> { + let name = moq_net::Path::new(&name)?; let (addr, resource) = parse_url(&url).await?; tracing::info!(url = %RedactedUrl::new(&url), %name, "SRT client pushing"); notify_ready(); diff --git a/rs/moq-cli/src/subscribe.rs b/rs/moq-cli/src/subscribe.rs index ecd39d0bea9..cde1f32cc9d 100644 --- a/rs/moq-cli/src/subscribe.rs +++ b/rs/moq-cli/src/subscribe.rs @@ -250,7 +250,7 @@ impl SubscribeArgs { /// Exports one broadcast from the Origin to stdout in the requested format. pub struct Subscribe { origin: moq_net::origin::Consumer, - path: moq_net::PathOwned, + path: moq_net::Path, source: moq_mux::Source, catalog: CatalogFormat, args: SubscribeArgs, @@ -281,7 +281,7 @@ impl Subscribe { catalog: CatalogFormat, args: SubscribeArgs, ) -> anyhow::Result { - let path = moq_net::Path::new(path).to_owned(); + let path = moq_net::Path::new(path)?; let broadcast = origin .request_broadcast(&path, None) .await diff --git a/rs/moq-cli/src/transcode.rs b/rs/moq-cli/src/transcode.rs index 50cf6667c32..44209bcff7a 100644 --- a/rs/moq-cli/src/transcode.rs +++ b/rs/moq-cli/src/transcode.rs @@ -106,19 +106,19 @@ pub async fn run(moq: MoqSide, args: Args, net: Net) -> anyhow::Result<()> { }; config.resize.output = args.frames.0; - let source_path = moq_net::PathOwned::from( + let source_path = moq_net::Path::try_from( moq.broadcast .clone() .context("`transcode` requires the source broadcast: pass --broadcast ")?, - ); + )?; if source_path.is_empty() { anyhow::bail!("`transcode` requires the source broadcast: pass --broadcast "); } - let output_path = moq_net::PathOwned::from( + let output_path = moq_net::Path::try_from( args.output .clone() .unwrap_or_else(|| format!("{source_path}/transcode.hang")), - ); + )?; // Publish the derivative through one origin and consume the source through // another, over a single auto-reconnecting session. @@ -138,7 +138,10 @@ pub async fn run(moq: MoqSide, args: Args, net: Net) -> anyhow::Result<()> { // Point the derivative catalog at the source renditions so players fetch them from the // source directly. An empty reference would name the derivative broadcast itself, which // publishes the rungs and nothing else. - config.source = source_path.relative(&output_path).filter(|rel| !rel.is_empty()); + let source = source_path + .relative(&output_path) + .with_context(|| format!("`{output_path}` cannot reference the source broadcast `{source_path}`"))?; + config.source = Some(source).filter(|rel| !rel.is_empty()); let following = Following { origin: remote.consume(), @@ -158,8 +161,8 @@ pub async fn run(moq: MoqSide, args: Args, net: Net) -> anyhow::Result<()> { struct Following { origin: moq_net::origin::Consumer, publish: moq_net::origin::Producer, - source_path: moq_net::PathOwned, - output_path: moq_net::PathOwned, + source_path: moq_net::Path, + output_path: moq_net::Path, config: moq_transcode::Config, } @@ -232,8 +235,8 @@ mod tests { let following = Following { origin: origin.consume(), publish: publish.clone(), - source_path: "source".into(), - output_path: "derived".into(), + source_path: hang::moq_net::Path::from_static("source"), + output_path: hang::moq_net::Path::from_static("derived"), config: Default::default(), }; let task = tokio::spawn(following.run()); @@ -279,8 +282,8 @@ mod tests { let following = Following { origin: origin.consume(), publish: publish.clone(), - source_path: "source".into(), - output_path: "derived".into(), + source_path: hang::moq_net::Path::from_static("source"), + output_path: hang::moq_net::Path::from_static("derived"), config: Default::default(), }; let task = tokio::spawn(following.run()); diff --git a/rs/moq-cli/tests/integration/epochs.rs b/rs/moq-cli/tests/integration/epochs.rs index 9997d8eaa23..01aba9abfcb 100644 --- a/rs/moq-cli/tests/integration/epochs.rs +++ b/rs/moq-cli/tests/integration/epochs.rs @@ -95,7 +95,7 @@ fn publish(origin: &origin::Producer, epoch: &Epoch, tag: &str) -> Published { catalog .mutate(|catalog| { let mut audio = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2); - audio.broadcast = Some(moq_net::path::Relative::new("./media").into_owned()); + audio.broadcast = Some(moq_net::path::Relative::new("./media").unwrap()); audio.epoch = Some(epoch.clone()); catalog.audio.renditions.insert("audio".into(), audio); }) diff --git a/rs/moq-e2ee/src/credential.rs b/rs/moq-e2ee/src/credential.rs index 26f6940cb4a..fb709bd24fc 100644 --- a/rs/moq-e2ee/src/credential.rs +++ b/rs/moq-e2ee/src/credential.rs @@ -83,9 +83,10 @@ impl Credential { /// # Errors /// /// [`Error::Identity`](crate::Error::Identity) if `semantic` exceeds 65535 bytes. - pub fn path(&self, semantic: &str) -> Result { + pub fn path(&self, semantic: &str) -> Result { let material: [u8; PATH_LEN] = self.expand(&self.path_info(semantic)?); - Ok(moq_net::Path::new(&URL_SAFE_NO_PAD.encode(material)).into_owned()) + // A fixed-length encoding, far under the path limit. + Ok(moq_net::Path::new(&URL_SAFE_NO_PAD.encode(material)).expect("a fixed-length path fits")) } /// Mint a fresh epoch and a producing generation whose clones share track claims. diff --git a/rs/moq-ffi/src/consumer.rs b/rs/moq-ffi/src/consumer.rs index 33c70c992cc..574e0e6f02c 100644 --- a/rs/moq-ffi/src/consumer.rs +++ b/rs/moq-ffi/src/consumer.rs @@ -239,7 +239,10 @@ impl MoqBroadcastConsumer { .watch(async { // Normalize before testing emptiness: this is a caller-supplied string, and one made only // of slashes normalizes to the empty reference, which names this broadcast. - let reference = reference.map(moq_net::path::Relative::new); + let reference = reference + .map(moq_net::path::Relative::new) + .transpose() + .map_err(moq_net::Error::from)?; let epoch: Option = epoch.map(str::parse).transpose()?; let Some(origin) = self.origin.clone() else { diff --git a/rs/moq-ffi/src/media.rs b/rs/moq-ffi/src/media.rs index a7203d1e192..8cf6649cb72 100644 --- a/rs/moq-ffi/src/media.rs +++ b/rs/moq-ffi/src/media.rs @@ -625,14 +625,14 @@ mod test { let epoch: moq_net::Epoch = crate::origin::moq_mint_epoch().parse().unwrap(); let mut video = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8); - video.broadcast = Some(moq_net::path::Relative::new("./source").into_owned()); + video.broadcast = Some(moq_net::path::Relative::new("./source").unwrap()); video.epoch = Some(epoch.clone()); catalog.video.renditions.insert("video".to_string(), video); let local = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8); catalog.video.renditions.insert("local".to_string(), local); let mut audio = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2); - audio.broadcast = Some(moq_net::path::Relative::new("../elsewhere").into_owned()); + audio.broadcast = Some(moq_net::path::Relative::new("../elsewhere").unwrap()); catalog.audio.renditions.insert("audio".to_string(), audio); let converted = convert_catalog(&catalog); diff --git a/rs/moq-gst/src/source/imp.rs b/rs/moq-gst/src/source/imp.rs index 9c2916f8bef..075ff496262 100644 --- a/rs/moq-gst/src/source/imp.rs +++ b/rs/moq-gst/src/source/imp.rs @@ -390,7 +390,7 @@ struct Shape { caps: gst::Caps, container: hang::catalog::Container, track: String, - broadcast: Option, + broadcast: Option, epoch: Option, } @@ -728,7 +728,7 @@ async fn follow_catalog( shutdown: shutdown.clone(), stop: stopped, }; - let mut followers: HashMap = HashMap::new(); + let mut followers: HashMap = HashMap::new(); // Woken when a follower runs out of renditions, so the run can let it go without waiting for // another catalog update. let emptied = Arc::new(tokio::sync::Notify::new()); @@ -799,7 +799,7 @@ fn reconcile( catalog: &moq_mux::catalog::hang::Catalog, source: &moq_mux::Source, playback: &Playback, - followers: &mut HashMap, + followers: &mut HashMap, tasks: &mut tokio::task::JoinSet<()>, emptied: &Arc, cancel: &watch::Receiver, @@ -871,7 +871,7 @@ fn reconcile( tasks.spawn_on(STREAMING.scope(playback.element.clone(), retire), RUNTIME.handle()); } - let mut plans: HashMap = HashMap::new(); + let mut plans: HashMap = HashMap::new(); for (rendition, shape) in desired { let Some(path) = source.resolve_reference(shape.broadcast.as_ref()) else { gst::warning!( @@ -888,7 +888,7 @@ fn reconcile( // one playing out its tail. Its follower stays reachable, with its finished records, while a // disabled rendition still names it or a delisted one drains, so a re-enable or relist reaches // it rather than a new follower. Once it plays nothing, the run lets it go. - let held: HashSet = disabled + let held: HashSet = disabled .iter() .filter_map(|(_, reference)| source.resolve_reference(reference.broadcast())) .collect(); @@ -998,7 +998,7 @@ struct Plan { /// rendition is reading, idle, or playing out its tail is its own state rather than a task's. struct Follower { playback: Playback, - path: moq_net::PathOwned, + path: moq_net::Path, renditions: HashMap, readers: tokio::task::JoinSet<(Rendition, u64, Exit)>, /// The instance last started. @@ -2264,7 +2264,7 @@ mod session_tests { let mut catalog = moq_mux::catalog::Producer::new(&mut catalog_broadcast.broadcast().clone(), Default::default()).unwrap(); let mut config = video_rendition(); - config.broadcast = Some("./left".into()); + config.broadcast = Some(moq_net::path::Relative::new("./left").unwrap()); config.track = Some("encoded".into()); catalog.modify().unwrap().video.renditions.insert("view".into(), config); catalog_broadcast.announce(Default::default()).unwrap(); @@ -2295,7 +2295,7 @@ mod session_tests { .renditions .get_mut("view") .unwrap() - .broadcast = Some("./right".into()); + .broadcast = Some(moq_net::path::Relative::new("./right").unwrap()); let next = super::RUNTIME .block_on(async { tokio::time::timeout(Duration::from_secs(10), replacement.requested_track()).await }); stopping.send(true).unwrap(); @@ -3572,7 +3572,7 @@ mod session_tests { /// A `room` whose video rendition references the `cam` sibling, naming `epoch` when given. fn referencing(origin: &moq_net::origin::Producer, epoch: Option) -> Publisher { let mut config = video_rendition(); - config.broadcast = Some("./cam".into()); + config.broadcast = Some(moq_net::path::Relative::new("./cam").unwrap()); config.epoch = epoch; Publisher::new(origin, Default::default(), config) } @@ -4271,7 +4271,7 @@ mod session_tests { let mut guard = room.catalog.modify().unwrap(); let config = guard.video.renditions.get_mut("video").unwrap(); config.track = None; - config.broadcast = Some("./cam".into()); + config.broadcast = Some(moq_net::path::Relative::new("./cam").unwrap()); } assert_eq!(recorder.written(&mut cam, 4), pad, "the move took another pad"); room.write(3); diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index ded016b871a..931f74ec086 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -784,7 +784,7 @@ mod tests { settle().await; let upstream = crate::resolved(&origin, "a/pub").await; let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8); - config.broadcast = Some(moq_net::path::Relative::new("../../source").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("../../source").unwrap()); let mut catalog = moq_mux::catalog::hang::Catalog::default(); catalog.archive = Some(archive(&["video0", "audio0"])); catalog.video.renditions.insert("video".to_string(), config); @@ -3446,7 +3446,7 @@ mod tests { let source = crate::resolved(&origin, "live").await; let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8); if sibling { - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); } let (rendition, watcher) = export(&source, &config); rendition.playable().await; @@ -3710,7 +3710,7 @@ mod tests { // A relative reference replaces the base's last segment, so "media" is a sibling of the // catalog broadcast "live". let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); let (rendition, watcher) = export(&upstream, &config); tokio::time::timeout(Duration::from_secs(5), rendition.playable()) @@ -3831,7 +3831,7 @@ mod tests { let upstream = crate::resolved(&origin, "live").await; let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); let mut snapshot = moq_mux::catalog::hang::Catalog::default(); snapshot.video.renditions.insert("video0".to_string(), config.clone()); @@ -3907,7 +3907,7 @@ mod tests { let upstream = crate::resolved(&origin, "live").await; let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8); - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); let mut snapshot = moq_mux::catalog::hang::Catalog::default(); snapshot.video.renditions.insert("video0".to_string(), config); @@ -3966,7 +3966,7 @@ mod tests { let upstream = crate::resolved(&origin, "live").await; let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); let (rendition, watcher) = export(&upstream, &config); tokio::time::timeout(Duration::from_secs(5), rendition.playable()) @@ -4005,7 +4005,7 @@ mod tests { let upstream = crate::resolved(&origin, "live").await; let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); let (rendition, watcher) = export(&upstream, &config); old_media.announce(sibling_route(10)).unwrap(); @@ -4065,7 +4065,7 @@ mod tests { let upstream = crate::resolved(&origin, "live").await; let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); let (rendition, watcher) = export(&upstream, &config); media.announce(sibling_route(10)).unwrap(); tokio::time::timeout(Duration::from_secs(5), rendition.playable()) @@ -4115,7 +4115,7 @@ mod tests { let upstream = crate::resolved(&origin, "live").await; let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("media").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("media").unwrap()); let (rendition, watcher) = export(&upstream, &config); tokio::time::timeout(Duration::from_secs(5), rendition.playable()) diff --git a/rs/moq-hls/src/export/rendition.rs b/rs/moq-hls/src/export/rendition.rs index be87738cde9..7fdff9b33a5 100644 --- a/rs/moq-hls/src/export/rendition.rs +++ b/rs/moq-hls/src/export/rendition.rs @@ -1494,11 +1494,11 @@ mod tests { let dynamic = origin.requests("room/secret").unwrap(); let source = moq_mux::Source::new(origin.consume(), broadcast.consume()); let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8); - config.broadcast = Some(moq_net::path::Relative::new("./secret").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("./secret").unwrap()); let rendition = Arc::new(Rendition::video("video".into(), &config, &source, Archive::default(), None).unwrap()); let request = dynamic.requested_broadcast().await.unwrap(); let other = origin.requests("room/hidden").unwrap(); - config.broadcast = Some(moq_net::path::Relative::new("./hidden").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("./hidden").unwrap()); let resolving = Arc::new(Rendition::video("video".into(), &config, &source, Archive::default(), None).unwrap()); let other_request = other.requested_broadcast().await.unwrap(); let row = |reference: &str| segments::Row { diff --git a/rs/moq-hls/src/server/routes.rs b/rs/moq-hls/src/server/routes.rs index 404cb6a5412..b5229f59573 100644 --- a/rs/moq-hls/src/server/routes.rs +++ b/rs/moq-hls/src/server/routes.rs @@ -376,7 +376,7 @@ mod tests { let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let reserved = catalog.reserve(); let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("./secret").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("./secret").to_owned().unwrap()); let mut video = reserved .video( secret.create_track("video0", timed()).unwrap(), @@ -603,7 +603,7 @@ mod tests { let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let reserved = catalog.reserve(); let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("./source").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("./source").unwrap()); let mut allowed = reserved .video( source.create_track("video0", timed()).unwrap(), @@ -612,7 +612,7 @@ mod tests { ) .unwrap(); let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("./secret").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("./secret").unwrap()); let mut denied = reserved .video( secret.create_track("video1", timed()).unwrap(), @@ -671,7 +671,7 @@ mod tests { // restricted server's export into using them. let broad = moq_mux::Source::new(origin.consume(), broadcast.consume()); let referenced = broad - .resolve(Some(&moq_net::path::Relative::new("./secret")), None) + .resolve(Some(&moq_net::path::Relative::new("./secret").unwrap()), None) .await .unwrap(); let mut group = referenced.track("video1").unwrap().fetch_group(0, None).await.unwrap(); diff --git a/rs/moq-hls/tests/integration/server.rs b/rs/moq-hls/tests/integration/server.rs index 1cc94e6c780..be5d27abdf4 100644 --- a/rs/moq-hls/tests/integration/server.rs +++ b/rs/moq-hls/tests/integration/server.rs @@ -181,7 +181,7 @@ async fn a_sibling_resolves_through_a_client_session() { let source = moq_mux::Source::new(origin, broadcast); let sibling = moq_clock::timeout( TIMEOUT, - source.resolve(Some(&moq_net::path::Relative::new("./source")), None), + source.resolve(Some(&moq_net::path::Relative::new("./source").unwrap()), None), ) .await .expect("sibling resolve timed out") @@ -204,7 +204,7 @@ async fn a_disconnected_sibling_publisher_answers_404() { moq_mux::catalog::Producer::new(&mut catalog_broadcast.broadcast().clone(), Default::default()).unwrap(); let reserved = catalog.reserve(); let mut config = video_config(); - config.broadcast = Some(moq_net::path::Relative::new("./source").to_owned()); + config.broadcast = Some(moq_net::path::Relative::new("./source").unwrap()); let track = media_broadcast.create_track(TRACK, timed()).unwrap(); let mut media = reserved .video( diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index 0b4e5326dc6..50e1f68aa9b 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `moq_net::Path` has no lifetime + +`Source::resolve_reference` takes `Option<&Relative>` and returns `Option`, `ts::Programs::new` takes `impl Into`, so pass a `Path`, and `follow::Instances::new` takes `impl TryInto>`. Replace `PathOwned` with `Path` and drop `to_owned()` and `into_owned()` on paths. See moq-net's migration for the rest. + ### MKV and FLV import refuse malformed tracks and unsupported codecs (#6908) MKV import fails on a malformed track, a duplicate or zero `TrackNumber`, a zero `TimestampScale`, a block timestamp outside the `u64` timeline, an unsupported codec or subtitle encoding, or input truncated at `finish()`, instead of skipping the track or wrapping the timestamp. Input after `finish()` fails with `mkv::Error::TracksAfterFinish`. diff --git a/rs/moq-mux/benches/catalog.rs b/rs/moq-mux/benches/catalog.rs index 0de487b331f..d354f486719 100644 --- a/rs/moq-mux/benches/catalog.rs +++ b/rs/moq-mux/benches/catalog.rs @@ -49,7 +49,7 @@ struct Fanout { impl Fanout { async fn new(tracks: usize, subscribers: usize, references: bool) -> Self { let mut info = moq_net::broadcast::Info::new(); - info.path = "room/catalog".into(); + info.path = moq_net::Path::from_static("room/catalog"); let mut broadcast = info.produce(); let catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let tracks = (0..tracks) @@ -58,7 +58,7 @@ impl Fanout { let track = broadcast.create_track(format!("v{index}"), info).unwrap(); let mut config = hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8); if references { - config.broadcast = Some(format!("./source-{index}").into()); + config.broadcast = Some(moq_net::path::Relative::new(&format!("./source-{index}")).unwrap()); } catalog.video(track, Container::Legacy(Kind::Video), config).unwrap() }) @@ -111,7 +111,7 @@ impl Fanout { black_box(rendition.jitter); if let Some(reference) = &rendition.broadcast { assert!( - moq_net::Path::new("room/catalog") + moq_net::Path::from_static("room/catalog") .try_resolve(reference) .unwrap() .as_str() diff --git a/rs/moq-mux/benches/ts_programs.rs b/rs/moq-mux/benches/ts_programs.rs index 5c9e3a7763f..cc493044346 100644 --- a/rs/moq-mux/benches/ts_programs.rs +++ b/rs/moq-mux/benches/ts_programs.rs @@ -212,7 +212,11 @@ fn multiplex(programs: u16, extra: Extra, bitrate: u64) -> Vec { /// A fresh importer, with the origin driver it needs kept alive but unpolled. fn setup() -> (moq_mux::container::ts::Programs, impl Sized) { let (origin, driver) = moq_net::origin::Producer::new(Default::default()); - let programs = moq_mux::container::ts::Programs::new(origin, "bench.hang", moq_mux::catalog::Config::default()); + let programs = moq_mux::container::ts::Programs::new( + origin, + moq_net::Path::from_static("bench.hang"), + moq_mux::catalog::Config::default(), + ); (programs, driver) } diff --git a/rs/moq-mux/src/binary.rs b/rs/moq-mux/src/binary.rs index 35561654795..4b562515108 100644 --- a/rs/moq-mux/src/binary.rs +++ b/rs/moq-mux/src/binary.rs @@ -911,7 +911,7 @@ mod test { fn a_broadcast_reference_is_refused() { let (mut broadcast, catalog) = catalog(); let mut entry = mavlink(1); - entry.config.broadcast = Some(moq_net::path::RelativeOwned::new("source")); + entry.config.broadcast = Some(moq_net::path::Relative::new("source").unwrap()); assert!(matches!( catalog.binary_stream(track(&mut broadcast, "telemetry"), entry), diff --git a/rs/moq-mux/src/catalog/hang/consumer.rs b/rs/moq-mux/src/catalog/hang/consumer.rs index 0de524d4b78..cb8599080a8 100644 --- a/rs/moq-mux/src/catalog/hang/consumer.rs +++ b/rs/moq-mux/src/catalog/hang/consumer.rs @@ -119,7 +119,7 @@ mod test { fn referencing(rel: &str) -> hang::catalog::AudioConfig { let mut config = opus(); - config.broadcast = Some(moq_net::path::Relative::new(rel).into_owned()); + config.broadcast = Some(moq_net::path::Relative::new(rel).unwrap()); config } @@ -216,7 +216,7 @@ mod test { fn soliciting(reference: &str) -> Catalog { let (mut catalog, _) = catalog_payload("here"); catalog.echo = Some(hang::catalog::Echo::new( - moq_net::path::Relative::new(reference).into_owned(), + moq_net::path::Relative::new(reference).unwrap(), )); catalog } @@ -327,7 +327,7 @@ mod test { published.audio.renditions.insert("here".to_string(), opus()); let mut text = hang::catalog::TextConfig::new(hang::catalog::TextFormat::Vtt); - text.broadcast = Some(moq_net::path::Relative::new("../../../elsewhere").into_owned()); + text.broadcast = Some(moq_net::path::Relative::new("../../../elsewhere").unwrap()); published.text.renditions.insert("captions".to_string(), text); match publish_catalog(published) { @@ -346,16 +346,16 @@ mod test { let mut published = Catalog::<()>::default(); published.audio.renditions.insert("here".to_string(), opus()); - let escaping = moq_net::path::Relative::new("../../../elsewhere").into_owned(); + let escaping = moq_net::path::Relative::new("../../../elsewhere"); match section { "json" => { let mut config = hang::catalog::JsonConfig::new(hang::catalog::Mode::Stream); - config.broadcast = Some(escaping); + config.broadcast = Some(escaping.unwrap()); published.json.tracks.insert("chat".to_string(), config); } _ => { let mut config = hang::catalog::BinaryConfig::new(hang::catalog::Mode::Stream); - config.broadcast = Some(escaping); + config.broadcast = Some(escaping.unwrap()); published.binary.tracks.insert("blobs".to_string(), config); } } diff --git a/rs/moq-mux/src/catalog/msf/consumer.rs b/rs/moq-mux/src/catalog/msf/consumer.rs index 57e6ad9c877..b4621445aef 100644 --- a/rs/moq-mux/src/catalog/msf/consumer.rs +++ b/rs/moq-mux/src/catalog/msf/consumer.rs @@ -125,11 +125,11 @@ pub(crate) fn from_msf(msf: &moq_msf::Catalog, base: &moq_net: .namespace .as_ref() .map(|namespace| { - let target = moq_net::Path::new(namespace); let unresolvable = || Error::UnresolvableNamespace { name: track.name.clone(), namespace: namespace.clone(), }; + let target = moq_net::Path::new(namespace).map_err(|_| unresolvable())?; // An MSF namespace names content; normalizing it must not select another namespace. if target.as_str() != namespace { return Err(unresolvable()); @@ -521,7 +521,7 @@ mod test { #[test] fn namespace_must_be_expressible_as_a_relative_broadcast() { - let base = moq_net::Path::new("room/catalog"); + let base = moq_net::Path::from_static("room/catalog"); for (namespace, expected) in [("room/catalog", None), ("", Some(".."))] { let mut track = moq_msf::Track::new("video", moq_msf::Packaging::Loc); track.namespace = Some(namespace.into()); @@ -582,7 +582,7 @@ mod test { "conference123/alice", "alice", ] { - let catalog = from_msf(&msf, &moq_net::Path::new(base)).unwrap(); + let catalog = from_msf(&msf, &moq_net::Path::new(base).unwrap()).unwrap(); assert!(catalog.video.renditions["1080p-video"].broadcast.is_none(), "{base}"); assert!(catalog.audio.renditions["audio"].broadcast.is_none(), "{base}"); } diff --git a/rs/moq-mux/src/catalog/producer.rs b/rs/moq-mux/src/catalog/producer.rs index 218f826fbfd..3252b6b0674 100644 --- a/rs/moq-mux/src/catalog/producer.rs +++ b/rs/moq-mux/src/catalog/producer.rs @@ -1481,7 +1481,7 @@ mod test { let mut producer = Producer::new(&mut broadcast, Config::default()).unwrap(); let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8); config.container = Container::Loc; - config.broadcast = Some("./left".into()); + config.broadcast = Some(moq_net::path::Relative::new("./left").unwrap()); producer .modify() .unwrap() @@ -1642,7 +1642,7 @@ mod test { #[test] fn cross_broadcast_renditions_round_trip_through_msf() { - let base = moq_net::Path::new("room/catalog"); + let base = moq_net::Path::from_static("room/catalog"); let catalog = hang::Catalog::<()>::from_str(r#"{ "video": {"renditions": {"view": {"broadcast": "./left", "track": "video", "codec": "vp8", "container": {"kind": "loc"}}}}, "audio": {"renditions": {"sound": {"broadcast": "./right", "track": "audio", "codec": "opus", "sampleRate": 48000, "numberOfChannels": 2, "container": {"kind": "loc"}}}}, @@ -1654,25 +1654,25 @@ mod test { let restored = crate::catalog::msf::from_msf(&msf, &base).unwrap(); assert_eq!( base.try_resolve(restored.video.renditions["video"].broadcast.as_ref().unwrap()), - Some("room/left".into()) + Some(moq_net::Path::from_static("room/left")) ); assert_eq!( base.try_resolve(restored.audio.renditions["audio"].broadcast.as_ref().unwrap()), - Some("room/right".into()) + Some(moq_net::Path::from_static("room/right")) ); assert_eq!( base.try_resolve(restored.text.renditions["text"].broadcast.as_ref().unwrap()), - Some("captions".into()) + Some(moq_net::Path::from_static("captions")) ); } #[test] fn msf_refuses_unresolvable_broadcast_references() { - let base = moq_net::Path::new("room/catalog"); + let base = moq_net::Path::from_static("room/catalog"); for reference in ["../../left", "../../../left"] { let mut catalog = hang::Catalog::<()>::default(); let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8); - config.broadcast = Some(reference.into()); + config.broadcast = Some(moq_net::path::Relative::new(reference).unwrap()); catalog.video.renditions.insert("video".into(), config); assert!(to_msf_media::<()>(&catalog, &base).tracks.is_empty()); assert!(to_msf_media::<()>(&catalog, &moq_net::Path::empty()).tracks.is_empty()); @@ -1683,12 +1683,12 @@ mod test { /// reference to such a broadcast is refused rather than read from the catalog's broadcast. #[test] fn msf_refuses_references_readers_would_take_as_the_catalog() { - let base = moq_net::Path::new("room/catalog"); + let base = moq_net::Path::from_static("room/catalog"); for reference in ["../catalog", "./catalog/room/catalog"] { let mut catalog = hang::Catalog::<()>::default(); let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8); config.container = Container::Loc; - config.broadcast = Some(reference.into()); + config.broadcast = Some(moq_net::path::Relative::new(reference).unwrap()); catalog.video.renditions.insert("video".into(), config); assert!(to_msf_media::<()>(&catalog, &base).tracks.is_empty(), "{reference}"); } @@ -1698,12 +1698,12 @@ mod test { /// the later of two same-named tracks wins there. #[test] fn msf_names_are_unique_per_namespace_and_the_last_wins_a_hang_id() { - let base = moq_net::Path::new("room/catalog"); + let base = moq_net::Path::from_static("room/catalog"); let mut catalog = hang::Catalog::<()>::default(); for (id, broadcast) in [("left", "./left"), ("right", "./right")] { let mut config = VideoConfig::new(hang::catalog::VideoCodec::VP8); config.track = Some("video".into()); - config.broadcast = Some(broadcast.into()); + config.broadcast = Some(moq_net::path::Relative::new(broadcast).unwrap()); catalog.video.renditions.insert(id.into(), config); } let msf = to_msf_media::<()>(&catalog, &base); @@ -1714,7 +1714,7 @@ mod test { assert_eq!(restored.video.renditions.len(), 1); assert_eq!( base.try_resolve(restored.video.renditions["video"].broadcast.as_ref().unwrap()), - Some("room/right".into()) + Some(moq_net::Path::from_static("room/right")) ); } diff --git a/rs/moq-mux/src/container/source.rs b/rs/moq-mux/src/container/source.rs index 66c914fde56..3bd72f2618d 100644 --- a/rs/moq-mux/src/container/source.rs +++ b/rs/moq-mux/src/container/source.rs @@ -73,7 +73,7 @@ enum SourceState { /// be, and the track's name there. #[derive(Clone, Debug, PartialEq, Eq)] struct Target { - path: moq_net::PathOwned, + path: moq_net::Path, epoch: Option, track: String, } @@ -790,14 +790,14 @@ mod tests { inline: true, }); config.container = Container::Legacy; - config.broadcast = broadcast.map(|b| Relative::new(b).into_owned()); + config.broadcast = broadcast.map(|rel| Relative::new(rel).unwrap()); config } fn audio(broadcast: Option<&str>) -> AudioConfig { let mut config = AudioConfig::new(AudioCodec::Opus, 48_000, 2); config.container = Container::Legacy; - config.broadcast = broadcast.map(|b| Relative::new(b).into_owned()); + config.broadcast = broadcast.map(|rel| Relative::new(rel).unwrap()); config } diff --git a/rs/moq-mux/src/container/test_util.rs b/rs/moq-mux/src/container/test_util.rs index bf81eeb7d9f..9db18a51a4c 100644 --- a/rs/moq-mux/src/container/test_util.rs +++ b/rs/moq-mux/src/container/test_util.rs @@ -172,7 +172,7 @@ impl Moving { config.description = Some(crate::codec::h264::build_avcc(&[Bytes::from_static(sps)], &[Bytes::from_static(PPS)]).unwrap()); config.track = Some(track.to_string()); - config.broadcast = broadcast.map(|broadcast| moq_net::path::Relative::new(broadcast).into_owned()); + config.broadcast = broadcast.map(|rel| moq_net::path::Relative::new(rel).unwrap()); config } } diff --git a/rs/moq-mux/src/container/ts/follower.rs b/rs/moq-mux/src/container/ts/follower.rs index f38b1d829fa..d3f70a8d6b2 100644 --- a/rs/moq-mux/src/container/ts/follower.rs +++ b/rs/moq-mux/src/container/ts/follower.rs @@ -87,7 +87,7 @@ pub struct Follower { export: Export>, state: State, instances: Instances, - path: moq_net::PathOwned, + path: moq_net::Path, serving: Serving, /// Taking the instances on hand when built, which replay the route the export resolved. priming: bool, diff --git a/rs/moq-mux/src/container/ts/health.rs b/rs/moq-mux/src/container/ts/health.rs index e0ca98f5537..8b1485a36c3 100644 --- a/rs/moq-mux/src/container/ts/health.rs +++ b/rs/moq-mux/src/container/ts/health.rs @@ -1000,7 +1000,11 @@ mod test { #[test] fn a_multiplex_counts_each_fault_once() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", crate::catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + crate::catalog::Config::default(), + ); let mut data = super::super::import::test::two_programs(); let mut corrupt = vec![0x47, 0x9f, 0xff, 0x10]; corrupt.resize(188, 0xff); diff --git a/rs/moq-mux/src/container/ts/programs.rs b/rs/moq-mux/src/container/ts/programs.rs index 141a353c089..433d036f135 100644 --- a/rs/moq-mux/src/container/ts/programs.rs +++ b/rs/moq-mux/src/container/ts/programs.rs @@ -29,7 +29,7 @@ use crate::catalog; /// `PMT_error_2`), since until then a PID may be a program's still to come. pub struct Programs { origin: moq_net::origin::Producer, - name: moq_net::PathOwned, + name: moq_net::Path, config: catalog::Config, demux: Demux, /// The latest whole PAT, from the first that lists a program, which seeds every program. @@ -108,10 +108,10 @@ impl Program { impl Programs { /// Split the multiplex into broadcasts named after `name`, each catalog built from `config`. - pub fn new(origin: moq_net::origin::Producer, name: impl moq_net::AsPath, config: catalog::Config) -> Self { + pub fn new(origin: moq_net::origin::Producer, name: impl Into, config: catalog::Config) -> Self { Self { origin, - name: name.as_path().to_owned(), + name: name.into(), config, demux: Demux::new(true), pat: None, @@ -475,7 +475,11 @@ mod test { #[tokio::test(start_paused = true)] async fn internal_program_abort_preserves_the_catalog_error() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); programs.decode(&two_programs()).unwrap(); let mut consumers: Vec<_> = programs.programs.iter().map(|p| p.catalog.consume().unwrap()).collect(); programs.abort(moq_net::Error::Transport("decode failed".into())); @@ -527,7 +531,11 @@ mod test { #[tokio::test] async fn a_pat_spanning_two_packets_is_read_whole() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin.clone(), + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let input = fifty_programs(); for chunk in input.chunks(100) { programs.decode(chunk).unwrap(); @@ -551,7 +559,11 @@ mod test { #[test] fn a_corrupt_pat_starts_no_program() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); // A null packet after each PAT confirms its sync byte. let mut null = vec![0x47, 0x1f, 0xff, 0x10]; null.resize(188, 0xff); @@ -587,7 +599,11 @@ mod test { #[tokio::test] async fn a_pat_in_two_sections_starts_every_program() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin.clone(), + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); programs.decode(&two_section_pat()).unwrap(); programs.finish().unwrap(); assert_eq!(programs.programs.len(), 2); @@ -600,7 +616,11 @@ mod test { #[tokio::test] async fn only_programs_a_pmt_describes_publish() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin.clone(), + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); // Pairs of programs share a PMT PID, so 6 waits on the PID that starts 7. let pmt_pid = |program: u16| 0x0100 + program / 2; let mut cc = 0; @@ -639,7 +659,11 @@ mod test { #[tokio::test] async fn a_later_pat_revises_the_waiting_programs() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin.clone(), + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut cc = 0; let mut data = psi_packets(0, &mut cc, &[], &pat_section(&[(1, 0x0100), (2, 0x0101)])); let revised = psi::section(0x00, 1, 1, 0, 0, &[0x00, 0x01, 0xe2, 0x00]); @@ -665,7 +689,11 @@ mod test { #[tokio::test] async fn a_program_starts_on_the_last_pmt_in_its_packet() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin.clone(), + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut data = psi_packets(0, &mut 0, &[], &pat_section(&[(1, 0x0100)])); let mut sections = pmt_section(1, 0, &[(MP2, 0x0062, &[])]); sections.extend(pmt_section(1, 1, &[(MP2, 0x0061, &[])])); @@ -681,7 +709,11 @@ mod test { #[tokio::test] async fn a_pmt_spanning_two_packets_starts_its_program() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin.clone(), + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut data = psi_packets(0, &mut 0, &[], &pat_section(&[(1, 0x0100)])); let mut private = vec![0x80, 200]; private.resize(202, 0); @@ -700,7 +732,11 @@ mod test { #[test] fn a_corrupt_pmt_counts_once_whoever_reads_it() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut null = vec![0x47, 0x1f, 0xff, 0x10]; null.resize(188, 0xff); let pmt = |program| pmt_section(program, 0, &[(MP2, 0x0060 + program, &[])]); @@ -717,7 +753,11 @@ mod test { assert_eq!(programs.stats().crc_error, 4); let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut cc = 0; let mut data = psi_packets(0, &mut cc, &[], &pat_section(&[(1, 0x0100), (2, 0x0200)])); data.extend(psi_packets(0x0100, &mut 0, &[], &pmt(1))); @@ -731,7 +771,11 @@ mod test { // The same move landing while the corrupt section is partway in: the program it moved // never saw that section begin, so the scan counts it. let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut cc = 0; let mut data = psi_packets(0, &mut cc, &[], &pat_section(&[(1, 0x0100), (2, 0x0200)])); data.extend(psi_packets(0x0100, &mut 0, &[], &pmt(1))); @@ -753,7 +797,11 @@ mod test { // After that move, a section start abandons the partial for another split corrupt // section, which the moved program reads from its start, so it alone counts it. let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut cc = 0; let mut data = psi_packets(0, &mut cc, &[], &pat_section(&[(1, 0x0100), (2, 0x0200)])); data.extend(psi_packets(0x0100, &mut 0, &[], &pmt(1))); @@ -771,7 +819,11 @@ mod test { // A corrupt section begun in the packet that starts a program sharing the PID: the new // program reads that packet, so it alone counts the section. let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut data = psi_packets(0, &mut 0, &[], &pat_section(&[(1, 0x0100), (2, 0x0100)])); let mut sections = pmt(1); sections.extend(corrupt(pmt_section(2, 0, &[(MP2, 0x0071, &private)]))); @@ -783,7 +835,11 @@ mod test { // A waiting program's PMT PID that a started program reads only as media: no program // reassembles PMTs there, so the scan counts the corrupt section. let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut data = psi_packets(0, &mut 0, &[], &pat_section(&[(1, 0x0100), (2, 0x0061)])); data.extend(psi_packets(0x0100, &mut 0, &[], &pmt(1))); data.extend(psi_packets(0x0061, &mut 0, &[], &corrupt(pmt(2)))); @@ -797,7 +853,11 @@ mod test { #[test] fn every_program_mints_its_own_epoch() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); programs.decode(&two_section_pat()).unwrap(); programs.finish().unwrap(); let epochs: Vec<_> = programs @@ -815,7 +875,11 @@ mod test { #[test] fn a_program_announces_once_it_declares_its_tracks() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let input = two_programs(); let mut packets = input.chunks(188); while programs.programs.is_empty() { @@ -847,7 +911,11 @@ mod test { #[test] fn input_ending_before_a_pat_fails() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut null = vec![0x47, 0x1f, 0xff, 0x10]; null.resize(188, 0xff); programs.decode(&null).unwrap(); @@ -860,7 +928,11 @@ mod test { #[test] fn input_ending_after_pmts_but_before_media_fails_unannounced() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let input = two_programs(); programs.decode(&input[..3 * 188]).unwrap(); @@ -881,7 +953,11 @@ mod test { #[test] fn a_partial_pat_does_not_hold_the_multiplex() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); programs.decode(&fifty_programs()[..188]).unwrap(); let mut null = vec![0x47, 0x1f, 0xff, 0x10]; null.resize(188, 0xff); @@ -897,7 +973,11 @@ mod test { #[test] fn a_corrupt_pat_beside_a_good_one_counts_once() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let mut sections = corrupt(pat_section(&[(1, 0x0100)])); sections.extend(pat_section(&[(1, 0x0100)])); let mut data = psi_packets(0, &mut 0, &[], §ions); @@ -920,7 +1000,11 @@ mod test { pkt }; let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let pmt = |program: u16| { psi_packets( program << 8, @@ -944,7 +1028,11 @@ mod test { #[tokio::test] async fn every_program_publishes_its_own_broadcast() { let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default()); + let mut programs = Programs::new( + origin.clone(), + moq_net::Path::from_static("event.hang"), + catalog::Config::default(), + ); let input = two_programs(); let before = moq_clock::wall(); diff --git a/rs/moq-mux/src/container/ts/routes_test.rs b/rs/moq-mux/src/container/ts/routes_test.rs index 7ee3197b082..6b38b79fb39 100644 --- a/rs/moq-mux/src/container/ts/routes_test.rs +++ b/rs/moq-mux/src/container/ts/routes_test.rs @@ -420,7 +420,11 @@ async fn an_mpts_counts_strays_once_both_pmts_are_in() { let data = mux.bytes(); let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", crate::catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + crate::catalog::Config::default(), + ); programs.decode(&data).unwrap(); let stats = programs.stats(); assert_eq!( @@ -464,7 +468,11 @@ async fn a_pmt_on_another_programs_pid_claims_nothing() { mux.fill(UNLISTED, 3); let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", crate::catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + crate::catalog::Config::default(), + ); programs.decode(&mux.bytes()).unwrap(); assert_eq!(programs.stats().streams[&UNLISTED].refused, 5); } @@ -482,7 +490,11 @@ async fn a_pmt_that_never_arrives_stops_holding_the_count() { mux.packet(CLOCK, Flags::pcr(501 * MS)).fill(UNLISTED, 2); let origin = crate::source::produce_origin(); - let mut programs = Programs::new(origin, "event.hang", crate::catalog::Config::default()); + let mut programs = Programs::new( + origin, + moq_net::Path::from_static("event.hang"), + crate::catalog::Config::default(), + ); programs.decode(&mux.bytes()).unwrap(); assert_eq!(programs.stats().streams[&UNLISTED].refused, 2); } @@ -521,7 +533,7 @@ async fn a_pid_keeps_one_row_through_its_lifecycle() { let mut one = Imported::new(); let mut programs = Programs::new( crate::source::produce_origin(), - "event.hang", + moq_net::Path::from_static("event.hang"), crate::catalog::Config::default(), ); let mut at = 0; diff --git a/rs/moq-mux/src/follow.rs b/rs/moq-mux/src/follow.rs index 9823777dd5d..f2e8decc63a 100644 --- a/rs/moq-mux/src/follow.rs +++ b/rs/moq-mux/src/follow.rs @@ -41,7 +41,7 @@ impl Instances { /// [`Source`](crate::Source) for each instance. pub fn new( origin: &moq_net::origin::Consumer, - path: impl moq_net::AsPath, + path: impl TryInto>, epoch: Option<&moq_net::Epoch>, ) -> crate::Result { let announced = origin.announced_broadcast(path, None)?; @@ -156,7 +156,7 @@ mod tests { let _catalog = origin.publish("a/pub", Default::default()).unwrap(); settle().await; let source = resolved(&origin, "a/pub").await; - let mut follow = source.follow(Some(&Relative::new("./source")), None).unwrap(); + let mut follow = source.follow(Some(&Relative::new("./source").unwrap()), None).unwrap(); quiet(&mut follow).await; let first = origin.publish("a/source", Default::default()).unwrap(); @@ -182,7 +182,7 @@ mod tests { let _old = origin.publish("a/source", route(&old_epoch)).unwrap(); settle().await; let source = resolved(&origin, "a/pub").await; - let rel = Relative::new("./source"); + let rel = Relative::new("./source").unwrap(); let mut any = source.follow(Some(&rel), None).unwrap(); let mut named = source.follow(Some(&rel), Some(&old_epoch)).unwrap(); let old = followed(&mut any).await.unwrap(); @@ -212,7 +212,7 @@ mod tests { settle().await; let source = resolved(&origin, "a/pub").await; let mut named = source - .follow(Some(&Relative::new("./source")), Some(&moq_net::Epoch::mint())) + .follow(Some(&Relative::new("./source").unwrap()), Some(&moq_net::Epoch::mint())) .unwrap(); let _sibling = origin.publish("a/source", Default::default()).unwrap(); assert!(followed(&mut named).await.is_some()); @@ -225,7 +225,7 @@ mod tests { let old = origin.publish("a/pub", Default::default()).unwrap(); settle().await; let source = resolved(&origin, "a/pub").await; - let mut follow = source.follow(Some(&Relative::new("./pub")), None).unwrap(); + let mut follow = source.follow(Some(&Relative::new("./pub").unwrap()), None).unwrap(); assert!(followed(&mut follow).await.unwrap().is_clone(&source.broadcast())); let _new = origin.publish("a/pub", Default::default()).unwrap(); diff --git a/rs/moq-mux/src/json.rs b/rs/moq-mux/src/json.rs index 98f44d19f7c..29809366e74 100644 --- a/rs/moq-mux/src/json.rs +++ b/rs/moq-mux/src/json.rs @@ -717,7 +717,7 @@ mod test { let mut broadcast = moq_net::broadcast::Info::new().produce(); let mut existing = JsonConfig::new(Mode::Snapshot); - existing.broadcast = Some(moq_net::path::RelativeOwned::new("source")); + existing.broadcast = Some(moq_net::path::Relative::new("source").unwrap()); let mut seed = crate::catalog::hang::Catalog::<()>::default(); seed.json.tracks.insert("chat".to_string(), existing.clone()); diff --git a/rs/moq-mux/src/source.rs b/rs/moq-mux/src/source.rs index fbd2bbf2586..385b2506e76 100644 --- a/rs/moq-mux/src/source.rs +++ b/rs/moq-mux/src/source.rs @@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex}; use std::task::{Poll, ready}; /// The broadcast held for each path, shared by a source's clones and its requests. -type Pins = Arc>>; +type Pins = Arc>>; /// The subscription side of an export: an origin and the catalog broadcast that drives it. /// @@ -42,7 +42,7 @@ type Pins = Arc> #[derive(Clone)] pub struct Source { origin: moq_net::origin::Consumer, - path: moq_net::PathOwned, + path: moq_net::Path, pins: Pins, /// What each rendition an export reads through this source has received. See /// [`stats`](Self::stats). @@ -110,7 +110,7 @@ impl Source { &self.origin } - pub(crate) fn path(&self) -> &moq_net::PathOwned { + pub(crate) fn path(&self) -> &moq_net::Path { &self.path } @@ -139,7 +139,7 @@ impl Source { /// The broadcast `path` is pinned to, or a lookup that waits for the path's first /// announcement and pins its answer, either checked against `epoch` on every read. - pub(crate) fn request_path(&self, path: &moq_net::PathOwned, epoch: Option<&moq_net::Epoch>) -> Binding { + pub(crate) fn request_path(&self, path: &moq_net::Path, epoch: Option<&moq_net::Epoch>) -> Binding { let bound = match self.pins.lock().expect("pins poisoned").get(path) { Some(pinned) => Bound::Ready(pinned.clone()), // Unpinned even with an epoch: an epochless route still answers, best effort, and @@ -167,7 +167,7 @@ impl Source { /// A missing or empty reference returns the catalog broadcast path. A valid reference /// may return the empty root path, which still names a broadcast. `None` means the /// reference walked above the root and names nothing. - pub fn resolve_reference(&self, rel: Option<&moq_net::path::Relative<'_>>) -> Option { + pub fn resolve_reference(&self, rel: Option<&moq_net::path::Relative>) -> Option { match rel.filter(|rel| !rel.is_empty()) { Some(rel) => self.path.try_resolve(rel), None => Some(self.path.clone()), @@ -178,7 +178,7 @@ impl Source { /// /// The erroring counterpart to [`Self::resolve_reference`], for the consumer side, where /// an escaping reference is a fault to report rather than a rendition to drop. - fn target(&self, rel: Option<&moq_net::path::Relative<'_>>) -> crate::Result { + fn target(&self, rel: Option<&moq_net::path::Relative>) -> crate::Result { self.resolve_reference(rel).ok_or_else(|| { let rel = rel.map_or("", |rel| rel.as_str()); tracing::error!(rel, catalog = %self.path, "broadcast reference escapes the root"); @@ -239,7 +239,7 @@ impl Source { /// no broadcast, so there is nothing to resolve. pub async fn resolve( &self, - rel: Option<&moq_net::path::Relative<'_>>, + rel: Option<&moq_net::path::Relative>, epoch: Option<&moq_net::Epoch>, ) -> crate::Result { self.bind(rel, epoch)?.broadcast().await @@ -262,7 +262,7 @@ impl Source { /// `epoch` on every read. pub fn bind( &self, - rel: Option<&moq_net::path::Relative<'_>>, + rel: Option<&moq_net::path::Relative>, epoch: Option<&moq_net::Epoch>, ) -> crate::Result { Ok(self.request_path(&self.target(rel)?, epoch)) @@ -283,7 +283,7 @@ impl Source { /// use it to honor cross-broadcast renditions without reimplementing the path math. pub async fn subscribe_track( &self, - rel: Option<&moq_net::path::Relative<'_>>, + rel: Option<&moq_net::path::Relative>, epoch: Option<&moq_net::Epoch>, name: &str, ) -> crate::Result { @@ -306,7 +306,7 @@ impl Source { /// Rejects an escaping reference exactly as [`resolve`](Self::resolve) does. pub fn follow( &self, - rel: Option<&moq_net::path::Relative<'_>>, + rel: Option<&moq_net::path::Relative>, epoch: Option<&moq_net::Epoch>, ) -> crate::Result { let path = self.target(rel)?; @@ -365,7 +365,7 @@ enum Bound { routing: Box>, /// Where the answer is pinned, the first for its path winning. pins: Pins, - path: moq_net::PathOwned, + path: moq_net::Path, }, } @@ -476,7 +476,7 @@ mod tests { settle().await; let source = resolved(&origin, "pub").await; let dynamic = origin.requests("media").unwrap(); - let binding = source.bind(Some(&Relative::new("media")), None).unwrap(); + let binding = source.bind(Some(&Relative::new("media").unwrap()), None).unwrap(); let request = dynamic.requested_broadcast().await.unwrap(); assert!( @@ -511,7 +511,7 @@ mod tests { let _catalog = origin.publish("pub", Default::default()).unwrap(); settle().await; let source = resolved(&origin, "pub").await; - let media = Relative::new("media"); + let media = Relative::new("media").unwrap(); let binding = source.bind(Some(&media), None).unwrap(); settle().await; assert!(binding.poll_broadcast(&kio::Waiter::noop()).is_pending()); @@ -582,7 +582,7 @@ mod tests { .expect("the catalog resolves") .unwrap(); let source = Source::new(consumer, catalog); - let sibling = tokio::time::timeout(timeout, source.resolve(Some(&Relative::new("./source")), None)) + let sibling = tokio::time::timeout(timeout, source.resolve(Some(&Relative::new("./source").unwrap()), None)) .await .expect("the sibling resolves without the stalled session") .unwrap(); @@ -590,7 +590,7 @@ mod tests { // Nothing serves a later sibling yet and the stalled session never syncs, so its // lookup keeps waiting rather than failing, and resolves once it is published. - let binding = source.bind(Some(&Relative::new("./late")), None).unwrap(); + let binding = source.bind(Some(&Relative::new("./late").unwrap()), None).unwrap(); settle().await; assert!(binding.poll_broadcast(&kio::Waiter::noop()).is_pending()); let _late = healthy.publish("a/late", Default::default()).unwrap(); @@ -609,7 +609,7 @@ mod tests { let _old = origin.publish("a/source", Default::default()).unwrap(); settle().await; let source = resolved(&origin, "a/pub").await; - let sibling = Relative::new("./source"); + let sibling = Relative::new("./source").unwrap(); let first = source .resolve(Some(&sibling), None) .await @@ -644,7 +644,7 @@ mod tests { let _sibling = origin.publish("a/source", route(&sibling)).unwrap(); settle().await; let source = resolved(&origin, "a/pub").await; - let rel = Relative::new("./source"); + let rel = Relative::new("./source").unwrap(); let first = source.resolve(Some(&rel), None).await.unwrap(); assert_eq!(first.info().epoch.as_ref(), Some(&sibling)); @@ -668,7 +668,7 @@ mod tests { let _new = origin.publish("a/source", route(&new_epoch)).unwrap(); settle().await; let source = resolved(&origin, "a/pub").await; - let rel = Relative::new("./source"); + let rel = Relative::new("./source").unwrap(); assert!(is_mismatch( source.resolve(Some(&rel), Some(&old_epoch)).await, @@ -689,7 +689,7 @@ mod tests { let _old = origin.publish("a/source", route(&old_epoch)).unwrap(); settle().await; let source = resolved(&origin, "a/pub").await; - let rel = Relative::new("./source"); + let rel = Relative::new("./source").unwrap(); let first = source.resolve(Some(&rel), Some(&old_epoch)).await.unwrap(); let _new = origin.publish("a/source", route(&new_epoch)).unwrap(); @@ -724,7 +724,7 @@ mod tests { let source = resolved(&origin, "a/pub").await; for reference in [None, Some("./pub")] { - let rel = reference.map(Relative::new); + let rel = reference.map(|rel| Relative::new(rel).unwrap()); let own = source.resolve(rel.as_ref(), Some(&catalog)).await.unwrap(); assert!(own.is_clone(&source.broadcast()), "{reference:?}"); assert!(is_mismatch(source.resolve(rel.as_ref(), Some(&other)).await, &other)); @@ -746,8 +746,11 @@ mod tests { let source = resolved(&origin, "a/pub").await; let epoch = moq_net::Epoch::mint(); - let sibling = source.resolve(Some(&Relative::new("./source")), Some(&epoch)).await; - assert!(sibling.unwrap().info().epoch.is_none()); + let sibling = source + .resolve(Some(&Relative::new("./source").unwrap()), Some(&epoch)) + .await + .unwrap(); + assert!(sibling.info().epoch.is_none()); assert!(source.resolve(None, Some(&epoch)).await.is_ok()); } @@ -764,7 +767,7 @@ mod tests { settle().await; for reference in [None, Some(""), Some("live"), Some("./live"), Some("../a/live")] { - let rel = reference.map(Relative::new); + let rel = reference.map(|rel| Relative::new(rel).unwrap()); let bound = source.bind(rel.as_ref(), None).unwrap().broadcast().await.unwrap(); assert!( bound.is_closed(), @@ -825,7 +828,7 @@ mod tests { let source = resolved(&origin, "a/pub").await; // Names the catalog within its own parent. - let rel = Relative::new("./pub"); + let rel = Relative::new("./pub").unwrap(); source .subscribe_track(Some(&rel), None, "video") .await @@ -852,7 +855,7 @@ mod tests { // above the root. A lone `..` stops at the root, which still names a broadcast, so // it is not in this set. for reference in ["../../elsewhere", "../..", "../../.."] { - let rel = Relative::new(reference); + let rel = Relative::new(reference).unwrap(); assert!( source.resolve_reference(Some(&rel)).is_none(), "{reference} should escape" @@ -890,9 +893,9 @@ mod tests { level: 0x1e, inline: false, }); - escaped.broadcast = Some(Relative::new("../../source").to_owned()); + escaped.broadcast = Some(Relative::new("../../source").unwrap()); let mut sibling = escaped.clone(); - sibling.broadcast = Some(Relative::new("./source").to_owned()); + sibling.broadcast = Some(Relative::new("./source").unwrap()); let mut catalog = hang::Catalog::<()>::default(); catalog.video.renditions.insert("escaped".to_string(), escaped); @@ -938,9 +941,9 @@ mod tests { let source = created(&origin); let mut escaped = hang::catalog::TextConfig::new(hang::catalog::TextFormat::Vtt); - escaped.broadcast = Some(Relative::new("../../source").to_owned()); + escaped.broadcast = Some(Relative::new("../../source").unwrap()); let mut sibling = escaped.clone(); - sibling.broadcast = Some(Relative::new("./source").to_owned()); + sibling.broadcast = Some(Relative::new("./source").unwrap()); let mut catalog = hang::Catalog::<()>::default(); catalog.text.renditions.insert("escaped".to_string(), escaped); @@ -966,7 +969,7 @@ mod tests { let source = resolved(&origin, "a/pub").await; // The reference resolves to `a/source`, whose "video" track answers the subscribe. - let rel = Relative::new("./source"); + let rel = Relative::new("./source").unwrap(); source .subscribe_track(Some(&rel), None, "video") .await @@ -986,7 +989,7 @@ mod tests { settle().await; let source = resolved(&origin, "a/source/transcode").await; - let rel = Relative::new("."); + let rel = Relative::new(".").unwrap(); source .subscribe_track(Some(&rel), None, "video") .await @@ -1006,7 +1009,7 @@ mod tests { settle().await; let source = resolved(&origin, "top").await; - let rel = Relative::new("."); + let rel = Relative::new(".").unwrap(); source .subscribe_track(Some(&rel), None, "video") .await diff --git a/rs/moq-net-tests/tests/integration/ietf_conformance.rs b/rs/moq-net-tests/tests/integration/ietf_conformance.rs index 6548b8c5cbe..2f9b0ecdff1 100644 --- a/rs/moq-net-tests/tests/integration/ietf_conformance.rs +++ b/rs/moq-net-tests/tests/integration/ietf_conformance.rs @@ -22,7 +22,9 @@ async fn routing(version: &str, cluster: bool) { let publisher = origin(1); let relay = origin(2); let reader = origin(4); - let prefix = Arc::new(moq_net::fuzz::requests_route(&publisher, PREFIX, Default::default()).unwrap()); + let prefix = Arc::new( + moq_net::fuzz::requests_route(&publisher, moq_net::Path::new(PREFIX).unwrap(), Default::default()).unwrap(), + ); let broadcast = broadcast::Info::new().produce(); let track = broadcast.create_track("test", moq_net::track::Info::default()).unwrap(); let serving = prefix.clone(); diff --git a/rs/moq-net-tests/tests/integration/restart.rs b/rs/moq-net-tests/tests/integration/restart.rs index dec1ba05a49..3e15bd3e64a 100644 --- a/rs/moq-net-tests/tests/integration/restart.rs +++ b/rs/moq-net-tests/tests/integration/restart.rs @@ -181,7 +181,10 @@ struct Worker { impl Worker { fn new(hop: u64) -> Self { let origin = produce_origin(hop); - let claim = Rc::new(moq_net::fuzz::requests_route(&origin, "pool", origin::Route::default()).unwrap()); + let claim = Rc::new( + moq_net::fuzz::requests_route(&origin, moq_net::Path::from_static("pool"), origin::Route::default()) + .unwrap(), + ); let outputs = Rc::new(RefCell::new(Vec::new())); let (handler, served) = (claim.clone(), outputs.clone()); drop(moq_clock::sim::spawn(async move { @@ -266,7 +269,8 @@ impl Claim { fn new(origin: &origin::Producer, epoch: Option) -> Self { let mut route = origin::Route::default(); route.epoch = epoch; - let dynamic = Rc::new(moq_net::fuzz::requests_route(origin, "live", route).unwrap()); + let dynamic = + Rc::new(moq_net::fuzz::requests_route(origin, moq_net::Path::from_static("live"), route).unwrap()); let outputs = Rc::new(RefCell::new(Vec::new())); let (handler, served) = (dynamic.clone(), outputs.clone()); drop(moq_clock::sim::spawn(async move { diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index 15e213666c5..120c89c9728 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -20,7 +20,7 @@ default = ["async"] # Runtime-backed helpers and Lite/IETF sessions; explicit-time model drivers stay available. async = ["dep:futures", "moq-clock/runtime"] -# Exposes the wire codecs to the `fuzz/` harness and the announce, codec, varint, and frame benches, +# Exposes the wire codecs to the `fuzz/` harness and the announce, codec, path, varint, and frame benches, # and session views of an origin to the viewers bench, through the hidden `fuzz` module. Off by # default and hidden from the docs. fuzz = ["async"] @@ -204,5 +204,10 @@ name = "memory" harness = false required-features = ["async"] +[[bench]] +name = "path" +harness = false +required-features = ["fuzz"] + [lints] workspace = true diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 98b92dcbd1c..6d9205f22df 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,6 +4,19 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `Path` and `Relative` have no lifetime + +There is one owned `Path` type: a short path is stored inline and a long one shares a reference-counted buffer, so cloning stays cheap. Every path is at most `Path::MAX_BYTES` (16 KiB). + +- `PathOwned`, `Path<'a>`, and `Path<'static>` are all `Path`; `RelativeOwned` and `Relative<'a>` are `Relative`. +- `Path::{to_owned, into_owned, borrow}` and the `Relative` equivalents are gone: drop the call, or `clone()` a `&Path` you need to keep. +- `Path::new`, `Relative::new`, `Path::join`, and `Path::resolve` return `Result<_, path::TooLong>`, failing on a path longer than `Path::MAX_BYTES`. `Path::new` copies its argument; use `Path::from_static("room")` for a literal, which borrows it and panics if it is too long. +- `From<&str>`, `From<&String>`, and `From` for `Path` and `Relative` are `TryFrom`, with `path::TooLong` as the error. +- `AsPath` is gone. Origin entry points such as `publish`, `create_broadcast`, `scope`, `mount`, and `routed` take `impl TryInto>`, so `&str`, `String`, `Path`, and `&Path` still work and a too-long string fails with `Error::BoundsExceeded`. Other functions that took `impl AsPath`, such as `stats::Tier::new`, take `impl Into`: pass a `Path` or `&Path`. +- `origin::{Producer, Consumer}::absolute` and `announce::Consumer::absolute` return `Result`. +- `Path::{has_prefix, strip_prefix, join, relative}` take `&Path`: write `path.has_prefix(&Path::from_static("room"))`. +- Decoding a path longer than `Path::MAX_BYTES` fails with `BoundsExceeded`, like one deeper than `Path::MAX_PARTS`. + ### Request handlers are `requests()` ([#6752](https://github.com/moq-dev/moq/pull/6752)) Rename `origin::Producer::dynamic(prefix)`, `broadcast::Producer::dynamic()`, `track::Producer::dynamic()`, and `track::Request::dynamic()` to `requests`. diff --git a/rs/moq-net/benches/announce.rs b/rs/moq-net/benches/announce.rs index 42b4c870891..c649b648ce8 100644 --- a/rs/moq-net/benches/announce.rs +++ b/rs/moq-net/benches/announce.rs @@ -18,7 +18,7 @@ use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use moq_net::{ - Hop, Hops, PathOwned, announce, + Hop, Hops, Path, announce, fuzz::{AnnounceWriter, Announced, decode_announces, encode_announces}, }; @@ -52,7 +52,7 @@ fn initial(c: &mut Criterion) { .flat_map(|cost| { (0..routes).map(move |route| { announce::Event::Update(announce::Announce { - prefix: format!("route/{route}").into(), + prefix: moq_net::Path::new(&format!("route/{route}")).unwrap(), captures: None, route: moq_net::origin::Route::default().with_cost(cost), }) @@ -106,7 +106,7 @@ fn hops(ids: &[u64]) -> Hops { } /// Announce `routes` routes, then replace `churn` of them, oldest first. -fn stream(routes: u64, churn: u64, route: impl Fn(u64) -> (PathOwned, Hops)) -> Vec { +fn stream(routes: u64, churn: u64, route: impl Fn(u64) -> (Path, Hops)) -> Vec { let mut announced: Vec<_> = (0..routes) .map(|n| { let (path, hops) = route(n); @@ -121,7 +121,7 @@ fn stream(routes: u64, churn: u64, route: impl Fn(u64) -> (PathOwned, Hops)) -> announced } -fn health(n: u64) -> (PathOwned, Hops) { +fn health(n: u64) -> (Path, Hops) { let origin = n % ORIGINS; let path = format!( "{:x}/private/channel_{}/stream-health-{}", @@ -129,17 +129,17 @@ fn health(n: u64) -> (PathOwned, Hops) { n % 16, 1_700_000_000 + n ); - (path.into(), hops(&[id(origin), id(100), id(101)])) + (Path::new(&path).unwrap(), hops(&[id(origin), id(100), id(101)])) } -fn unique(n: u64) -> (PathOwned, Hops) { +fn unique(n: u64) -> (Path, Hops) { ( - format!("{:x}", id(n)).into(), + moq_net::Path::new(&format!("{:x}", id(n))).unwrap(), hops(&[id(n + 1_000_000), id(n + 2_000_000)]), ) } -type Workload = (&'static str, fn(u64) -> (PathOwned, Hops)); +type Workload = (&'static str, fn(u64) -> (Path, Hops)); const WORKLOADS: [Workload; 2] = [("health", health), ("unique", unique)]; fn report() { diff --git a/rs/moq-net/benches/origin.rs b/rs/moq-net/benches/origin.rs index 6900c88adbb..baa2457165d 100644 --- a/rs/moq-net/benches/origin.rs +++ b/rs/moq-net/benches/origin.rs @@ -168,7 +168,12 @@ fn bench_withdraw(c: &mut Criterion) { .unwrap(); let routes: Vec<_> = (0..publishers) .map(|i| { - moq_net::fuzz::requests_route(&producer, format!("room/{i}"), peer_route(9, 1)).unwrap() + moq_net::fuzz::requests_route( + &producer, + moq_net::Path::new(&format!("room/{i}")).unwrap(), + peer_route(9, 1), + ) + .unwrap() }) .collect(); let mut cursors: Vec<_> = (0..subscribers).map(|_| producer.consume().announced()).collect(); @@ -224,13 +229,22 @@ fn bench_withdraw_wave(c: &mut Criterion) { .unwrap(); let near: Vec<_> = (0..broadcasts) .map(|i| { - moq_net::fuzz::requests_route(&producer, format!("room/{i}"), route(&[9, 2])).unwrap() + moq_net::fuzz::requests_route( + &producer, + moq_net::Path::new(&format!("room/{i}")).unwrap(), + route(&[9, 2]), + ) + .unwrap() }) .collect(); let far: Vec<_> = (0..broadcasts) .map(|i| { - moq_net::fuzz::requests_route(&producer, format!("room/{i}"), route(&[9, 3, 4])) - .unwrap() + moq_net::fuzz::requests_route( + &producer, + moq_net::Path::new(&format!("room/{i}")).unwrap(), + route(&[9, 3, 4]), + ) + .unwrap() }) .collect(); let mut cursors: Vec<_> = (0..subscribers).map(|_| producer.consume().announced()).collect(); @@ -275,13 +289,29 @@ fn bench_route_churn(c: &mut Criterion) { group.bench_function(id, |b| { let (producer, _driver) = origin::Producer::new(origin::Config::default()); let _routes: Vec<_> = (0..broadcasts) - .map(|i| moq_net::fuzz::requests_route(&producer, format!("room/{i}"), peer_route(1, 1)).unwrap()) + .map(|i| { + moq_net::fuzz::requests_route( + &producer, + moq_net::Path::new(&format!("room/{i}")).unwrap(), + peer_route(1, 1), + ) + .unwrap() + }) .collect(); let mut cursors: Vec<_> = (0..subscribers).map(|_| producer.consume().announced()).collect(); for cursor in &mut cursors { while cursor.try_next().is_some() {} } - b.iter(|| drop(moq_net::fuzz::requests_route(&producer, "room/0", peer_route(2, 5)).unwrap())); + b.iter(|| { + drop( + moq_net::fuzz::requests_route( + &producer, + moq_net::Path::from_static("room/0"), + peer_route(2, 5), + ) + .unwrap(), + ) + }); for cursor in &mut cursors { assert!(cursor.try_next().is_none(), "the winner is unchanged"); } @@ -406,7 +436,12 @@ fn bench_announce_duplicate(c: &mut Criterion) { // the entries are distinct routes rather than one re-priced in place. let _routes: Vec<_> = (1..=duplicates) .map(|peer| { - moq_net::fuzz::requests_route(&producer, PATH, peer_route(peer as u64, INCUMBENT_COST)).unwrap() + moq_net::fuzz::requests_route( + &producer, + moq_net::Path::new(PATH).unwrap(), + peer_route(peer as u64, INCUMBENT_COST), + ) + .unwrap() }) .collect(); let mut cursors: Vec = (0..subscribers) @@ -419,7 +454,7 @@ fn bench_announce_duplicate(c: &mut Criterion) { b.iter(|| { let handle = moq_net::fuzz::requests_route( &producer, - PATH, + moq_net::Path::new(PATH).unwrap(), peer_route(duplicates as u64 + 1, INCUMBENT_COST - 1), ) .unwrap(); @@ -457,12 +492,21 @@ fn bench_reprice_duplicate(c: &mut Criterion) { &Patterns::from(Pattern::subtree(&format!("{PATH}/p{}", (peer - 1) % 2)).unwrap()), ) .unwrap(); - moq_net::fuzz::requests_route(&scoped, PATH, peer_route(peer as u64, incumbent_cost)).unwrap() + moq_net::fuzz::requests_route( + &scoped, + moq_net::Path::new(PATH).unwrap(), + peer_route(peer as u64, incumbent_cost), + ) + .unwrap() }) .collect(); let challenger = duplicates as u64 + 1; - let route = - moq_net::fuzz::requests_route(&producer, PATH, peer_route(challenger, incumbent_cost + 1)).unwrap(); + let route = moq_net::fuzz::requests_route( + &producer, + moq_net::Path::new(PATH).unwrap(), + peer_route(challenger, incumbent_cost + 1), + ) + .unwrap(); let mut cursors: Vec = (0..subscribers) .map(|peer| { producer @@ -718,10 +762,18 @@ fn bench_unroutable_churn(c: &mut Criterion) { group.bench_function(id, |b| { let (producer, mut driver) = origin::Producer::new(origin::Config::default()); let consumer = producer.consume(); - let server = - moq_net::fuzz::requests_route(&producer, "room", origin::Route::default()).unwrap(); - let unrelated = - moq_net::fuzz::requests_route(&producer, "other", origin::Route::default()).unwrap(); + let server = moq_net::fuzz::requests_route( + &producer, + moq_net::Path::from_static("room"), + origin::Route::default(), + ) + .unwrap(); + let unrelated = moq_net::fuzz::requests_route( + &producer, + moq_net::Path::from_static("other"), + origin::Route::default(), + ) + .unwrap(); let mut waiting = Waiting::new( (0..requesters) .map(|i| { @@ -845,7 +897,14 @@ fn pool(members: usize, paths: usize, cursors: usize) -> Pool { let consumer = producer.consume(); let cursors = (0..cursors).map(|_| consumer.announced()).collect(); let members: Vec<_> = (0..members) - .map(|i| moq_net::fuzz::requests_route(&producer, "pool", pool_route(POOL_HOP + i as u64)).unwrap()) + .map(|i| { + moq_net::fuzz::requests_route( + &producer, + moq_net::Path::from_static("pool"), + pool_route(POOL_HOP + i as u64), + ) + .unwrap() + }) .collect(); let waiter = kio::Waiter::noop(); @@ -904,7 +963,12 @@ fn bench_pool_churn(c: &mut Criterion) { let waiter = kio::Waiter::noop(); let hop = POOL_HOP + members as u64; b.iter(|| { - let joined = moq_net::fuzz::requests_route(&pool.producer, "pool", pool_route(hop)).unwrap(); + let joined = moq_net::fuzz::requests_route( + &pool.producer, + moq_net::Path::from_static("pool"), + pool_route(hop), + ) + .unwrap(); pool.driver.poll(moq_net::time::Instant::now(), &waiter).unwrap(); drop(joined); pool.driver.poll(moq_net::time::Instant::now(), &waiter).unwrap(); @@ -932,7 +996,12 @@ fn bench_pool_cursors(c: &mut Criterion) { let waiter = kio::Waiter::noop(); let hop = POOL_HOP + members as u64; b.iter(|| { - let joined = moq_net::fuzz::requests_route(&pool.producer, "pool", pool_route(hop)).unwrap(); + let joined = moq_net::fuzz::requests_route( + &pool.producer, + moq_net::Path::from_static("pool"), + pool_route(hop), + ) + .unwrap(); pool.driver.poll(moq_net::time::Instant::now(), &waiter).unwrap(); drop(joined); pool.driver.poll(moq_net::time::Instant::now(), &waiter).unwrap(); diff --git a/rs/moq-net/benches/path.rs b/rs/moq-net/benches/path.rs new file mode 100644 index 00000000000..566d08a3635 --- /dev/null +++ b/rs/moq-net/benches/path.rs @@ -0,0 +1,200 @@ +//! Allocations per path-heavy operation: building and slicing paths, decoding the +//! messages that carry them, resolving requests on an origin, and fanning +//! announcements out to its cursors. +//! +//! Counts are deterministic, so this prints them instead of timing: a regression +//! shows as a higher number, not as noise. Paths span the inline limit: short (10 +//! bytes), medium (22, the most that fits inline), and long (47). +//! +//! Run with `cargo bench -p moq-net --features fuzz --bench path`. + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::hint::black_box; +use std::sync::atomic::{AtomicU64, Ordering}; + +use futures::FutureExt; +use moq_net::{ + Hop, Hops, Path, Pattern, Patterns, announce, + fuzz::{Announced, Messages, decode_announces, decode_namespaces, encode_announces, encode_namespaces}, + kio, origin, +}; + +struct Counting; + +static ALLOCS: AtomicU64 = AtomicU64::new(0); + +// SAFETY: forwards every call to the system allocator unchanged, only counting. +unsafe impl GlobalAlloc for Counting { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + ALLOCS.fetch_add(1, Ordering::Relaxed); + // SAFETY: the caller upholds `alloc`'s contract. + unsafe { System.alloc(layout) } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + // SAFETY: the caller upholds `dealloc`'s contract. + unsafe { System.dealloc(ptr, layout) } + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new: usize) -> *mut u8 { + ALLOCS.fetch_add(1, Ordering::Relaxed); + // SAFETY: the caller upholds `realloc`'s contract. + unsafe { System.realloc(ptr, layout, new) } + } +} + +#[global_allocator] +static GLOBAL: Counting = Counting; + +/// Print the average allocations per item over `n` calls of `f`, each handling `per` items. +fn allocs(name: &str, n: u64, per: u64, mut f: impl FnMut() -> R) { + // Warm lazily built state first. + for _ in 0..16 { + black_box(f()); + } + let before = ALLOCS.load(Ordering::Relaxed); + for _ in 0..n { + black_box(f()); + } + let total = ALLOCS.load(Ordering::Relaxed) - before; + println!("{name:<40} {:>8.2}", total as f64 / (n * per) as f64); +} + +const SHORT: &str = "room/alice"; +const MEDIUM: &str = "anon/7f3a9c2e1b/camera"; +const LONG: &str = "1234/private/channel_7/stream-health-1700000000"; + +fn hops(ids: &[u64]) -> Hops { + Hops::try_from(ids.iter().map(|id| Hop::new(*id).unwrap()).collect::>()).unwrap() +} + +fn paths() { + for (name, s) in [("short", SHORT), ("medium", MEDIUM), ("long", LONG)] { + let path = Path::new(s).unwrap(); + let prefix = Path::new(s.split('/').next().unwrap()).unwrap(); + let cam = Path::from_static("cam"); + allocs(&format!("path/new/{name}"), 10_000, 1, || Path::new(black_box(s))); + allocs(&format!("path/clone/{name}"), 10_000, 1, || path.clone()); + allocs(&format!("path/strip_prefix/{name}"), 10_000, 1, || { + path.strip_prefix(black_box(&prefix)).unwrap() + }); + allocs(&format!("path/join/{name}"), 10_000, 1, || path.join(black_box(&cam))); + } +} + +fn decode() { + let messages = Messages::default(); + let mut lite = Vec::new(); + messages.encode_lite(&mut lite); + let mut ietf = Vec::new(); + messages.encode_ietf(&mut ietf); + allocs("decode/lite_mix", 10_000, 1, || messages.decode_lite(&lite)); + allocs("decode/ietf_mix", 10_000, 1, || messages.decode_ietf(&ietf)); + + type Shape = (&'static str, fn(u64) -> String); + let shapes: [Shape; 3] = [ + ("short", |i| format!("room/{i}")), + ("medium", |i| format!("anon/{:010x}/camera", i * 0x9E37_79B9)), + ("long", |i| { + format!( + "{}/private/channel_{}/stream-health-{}", + 1000 + i % 8, + i, + 1_700_000_000 + i + ) + }), + ]; + for (name, shape) in shapes { + let paths: Vec = (0..1000).map(|i| moq_net::Path::try_from(shape(i)).unwrap()).collect(); + let stream: Vec = paths + .iter() + .enumerate() + .map(|(i, path)| Announced::Start(path.clone(), hops(&[0x1111 + i as u64, 0x2222]))) + .collect(); + for compress in [false, true] { + let data = encode_announces(&stream, compress); + let version = if compress { "lite07" } else { "lite06" }; + allocs(&format!("decode/announce_{version}/{name}"), 50, 1000, || { + decode_announces(&data, compress) + }); + } + let data = encode_namespaces(&paths); + allocs(&format!("decode/announce_ietf/{name}"), 50, 1000, || { + decode_namespaces(&data) + }); + } +} + +fn requests() { + for (name, s) in [("short", SHORT), ("long", LONG)] { + let (producer, mut driver) = origin::Producer::new(origin::Config::default()); + let consumer = producer.consume(); + let _pubs: Vec<_> = (0..1000) + .map(|i| producer.publish(format!("{s}/{i}"), origin::Route::default()).unwrap()) + .collect(); + let _covering = producer.publish(s, origin::Route::default()).unwrap(); + let waiter = kio::Waiter::noop(); + + let mut local = |request: &dyn Fn() -> kio::Pending| { + let pending = request(); + driver.poll(moq_net::time::Instant::now(), &waiter).unwrap(); + pending.now_or_never().expect("resolves").expect("local broadcast") + }; + + let target = format!("{s}/0"); + let target_path = Path::new(&target).unwrap(); + allocs(&format!("origin/request_str/{name}"), 2_000, 1, || { + local(&|| consumer.request_broadcast(target.as_str(), None)) + }); + allocs(&format!("origin/request_path/{name}"), 2_000, 1, || { + local(&|| consumer.request_broadcast(&target_path, None)) + }); + + // A rooted handle joins its root onto every path it is given. + let (root, rest) = s.split_once('/').unwrap(); + let rooted = consumer.scope(root, &Patterns::from(Pattern::all())).unwrap(); + let rooted_target = format!("{rest}/0"); + allocs(&format!("origin/request_rooted_str/{name}"), 2_000, 1, || { + local(&|| rooted.request_broadcast(rooted_target.as_str(), None)) + }); + + let missing = format!("{s}/missing"); + allocs(&format!("origin/unroutable_str/{name}"), 2_000, 1, || { + let result = consumer.request_broadcast(missing.as_str(), None).now_or_never(); + assert!(matches!(result, Some(Err(moq_net::Error::Unroutable)))); + }); + } +} + +fn fanout() { + for (name, s) in [("short", "room/incoming"), ("long", LONG)] { + let (producer, _driver) = origin::Producer::new(origin::Config::default()); + let consumer = producer.consume(); + let _pubs: Vec<_> = (0..100) + .map(|i| producer.publish(format!("room/{i}"), origin::Route::default()).unwrap()) + .collect(); + let mut cursors: Vec = (0..100).map(|_| consumer.announced()).collect(); + for cursor in &mut cursors { + while cursor.next().now_or_never().flatten().is_some() {} + } + + // Per cursor, per publish and retract. + allocs(&format!("origin/announce_fanout/{name}"), 500, 100, || { + let publication = producer.publish(s, origin::Route::default()).unwrap(); + for cursor in &mut cursors { + cursor.next().now_or_never().flatten().expect("announce delivered"); + } + drop(publication); + for cursor in &mut cursors { + cursor.next().now_or_never().flatten().expect("retract delivered"); + } + }); + } +} + +fn main() { + paths(); + decode(); + requests(); + fanout(); +} diff --git a/rs/moq-net/benches/retirement.rs b/rs/moq-net/benches/retirement.rs index 365ec2b6a00..caf3427f0a8 100644 --- a/rs/moq-net/benches/retirement.rs +++ b/rs/moq-net/benches/retirement.rs @@ -32,7 +32,7 @@ fn retirement(c: &mut Criterion) { if selected { let watch = RequestValidity::new( route, - &Path::new(&format!("route/{i}/jobs/{reader}")), + &Path::new(&format!("route/{i}/jobs/{reader}")).unwrap(), ); pending.push(watch.start()); watches.push(watch); diff --git a/rs/moq-net/benches/stats.rs b/rs/moq-net/benches/stats.rs index cc6e1300db5..b35f04a31a8 100644 --- a/rs/moq-net/benches/stats.rs +++ b/rs/moq-net/benches/stats.rs @@ -28,7 +28,9 @@ fn registry(broadcasts: usize, tiers: usize) -> Registry { .collect(); let cursors = (0..tiers) .map(|t| { - let session = registry.tier(stats::Tier::new(format!("tier{t}"))).session("root"); + let session = registry + .tier(stats::Tier::new(moq_net::Path::new(&format!("tier{t}")).unwrap())) + .session(moq_net::Path::from_static("root")); let mut cursor = producer.consume().with_stats(session).announced(); // Each announce the cursor replays records under its tier. while cursor.next().now_or_never().flatten().is_some() {} @@ -71,15 +73,17 @@ fn request(c: &mut Criterion) { let registry = stats::Registry::new(stats::Config::new()); let mut contexts = Vec::new(); for tier in 0..tiers { - let handle = registry.tier(stats::Tier::new(format!("tier{tier}"))); + let handle = registry.tier(stats::Tier::new(moq_net::Path::new(&format!("tier{tier}")).unwrap())); for root in 0..roots { - contexts.push(handle.request(format!("room/{root}"))); + contexts.push(handle.request(moq_net::Path::new(&format!("room/{root}")).unwrap())); } } - let handle = registry.tier(stats::Tier::new(format!("tier{}", tiers - 1))); + let handle = registry.tier(stats::Tier::new( + moq_net::Path::new(&format!("tier{}", tiers - 1)).unwrap(), + )); let root = format!("room/{}", roots - 1); group.bench_function(BenchmarkId::from_parameter(format!("{roots}x{tiers}")), |b| { - b.iter(|| std::hint::black_box(handle.request(&root))); + b.iter(|| std::hint::black_box(handle.request(moq_net::Path::new(&root).unwrap()))); }); std::hint::black_box(contexts); } @@ -107,7 +111,8 @@ fn session_report(c: &mut Criterion) { let contexts: Vec<_> = (0..labels) .flat_map(|label| { let handle = handle.clone(); - (0..sessions).map(move |_| handle.session(format!("project/key/{label}"))) + (0..sessions) + .map(move |_| handle.session(moq_net::Path::new(&format!("project/key/{label}")).unwrap())) }) .collect(); let wires: Vec<_> = contexts @@ -153,7 +158,8 @@ fn retag(c: &mut Criterion) { let mut contexts: Vec<_> = (0..labels) .flat_map(|label| { let handle = handle.clone(); - (0..sessions).map(move |_| handle.session(format!("project/key/{label}"))) + (0..sessions) + .map(move |_| handle.session(moq_net::Path::new(&format!("project/key/{label}")).unwrap())) }) .collect(); let moving = contexts.split_off(contexts.len() - sessions); @@ -161,10 +167,13 @@ fn retag(c: &mut Criterion) { group.bench_function(BenchmarkId::from_parameter(format!("{labels}x{sessions}")), |b| { b.iter(|| { for session in moving.iter().rev() { - session.retag(tier.clone(), "project/moved"); + session.retag(tier.clone(), moq_net::Path::from_static("project/moved")); } for session in moving.iter().rev() { - session.retag(tier.clone(), format!("project/key/{}", labels - 1)); + session.retag( + tier.clone(), + moq_net::Path::new(&format!("project/key/{}", labels - 1)).unwrap(), + ); } }); }); diff --git a/rs/moq-net/src/auth.rs b/rs/moq-net/src/auth.rs index cb82eaf0e5b..2999046646c 100644 --- a/rs/moq-net/src/auth.rs +++ b/rs/moq-net/src/auth.rs @@ -1954,7 +1954,7 @@ struct Held { grant: Grant, deadline: crate::time::Deadline, /// The request this grant must keep covering: a replacement that no longer does ends it. - path: crate::PathOwned, + path: crate::Path, kind: RequestKind, /// The request's slot, shared with a renewal's verification. hold: Option, @@ -1971,7 +1971,7 @@ impl RequestGrant { clock: &crate::time::Deadlines, verdict: RequestVerdict, res: std::result::Result, - path: crate::PathOwned, + path: crate::Path, kind: RequestKind, hold: Option, ) -> std::result::Result { @@ -2197,7 +2197,7 @@ pub(crate) enum Direction { /// only re-matched when the union or the limit changes. pub(crate) struct Gate { handle: Link, - path: crate::PathOwned, + path: crate::Path, direction: Direction, epoch: u64, part: Part, @@ -2209,7 +2209,7 @@ pub(crate) struct Gate { /// Log the whole wait once, including when its request is cancelled. struct Waiting { - path: crate::PathOwned, + path: crate::Path, started: Instant, } @@ -2230,7 +2230,7 @@ enum Part { } impl Gate { - pub(crate) fn new(handle: Link, path: crate::PathOwned, direction: Direction) -> Self { + pub(crate) fn new(handle: Link, path: crate::Path, direction: Direction) -> Self { let within_limit = handle.within_limit(direction, path.as_str()); Self { handle, @@ -2252,12 +2252,7 @@ impl Gate { /// A gate for a request under a namespace the peer announced, while `live` stays open: /// the announce stands in for the scope, as its own token may have authorized it. - pub(crate) fn announced( - handle: Link, - path: crate::PathOwned, - direction: Direction, - live: kio::Consumer<()>, - ) -> Self { + pub(crate) fn announced(handle: Link, path: crate::Path, direction: Direction, live: kio::Consumer<()>) -> Self { Self { part: Part::Announced(live), ..Self::new(handle, path, direction) @@ -2416,7 +2411,7 @@ pub(crate) struct Enforce { epoch: u64, permit: Option, /// Every broadcast admitted so far, still announced. - live: std::collections::HashSet, + live: std::collections::HashSet, setup: bool, /// Broadcasts outside the union, still announced, waiting on tokens that might cover /// them, in announcement order. @@ -2431,7 +2426,7 @@ impl Enforce { handle: &Link, announced: &mut crate::announce::Consumer, waiter: &kio::Waiter, - ) -> Poll> { + ) -> Poll> { if !self.setup { ready_or!(handle.poll_setup_answered(waiter)); self.setup = true; @@ -2500,11 +2495,7 @@ mod tests { let setup = handle.present(Bytes::new(), true).unwrap(); handle.granted(setup.presented.id, Grant::default()); let token = handle.present(Bytes::from_static(b"new"), false).unwrap(); - let mut gate = Gate::new( - handle.clone(), - crate::Path::new("room").to_owned(), - Direction::Subscribe, - ); + let mut gate = Gate::new(handle.clone(), crate::Path::from_static("room"), Direction::Subscribe); let waiter = kio::Waiter::noop(); assert!( gate.poll_ready(&waiter).is_pending(), @@ -2526,7 +2517,7 @@ mod tests { let busy = Error::Session(SessionError::TooManyRequests); assert_eq!(handle.retry(id, &busy), Some(RETRY_INITIAL)); - let mut gate = Gate::new(handle.clone(), crate::Path::new("room").to_owned(), Direction::Publish); + let mut gate = Gate::new(handle.clone(), crate::Path::from_static("room"), Direction::Publish); let waiter = kio::Waiter::noop(); assert!( gate.poll_ready(&waiter).is_pending(), @@ -2549,7 +2540,7 @@ mod tests { let route = kio::Producer::<()>::default(); let mut gate = Gate::announced( handle, - crate::Path::new("room").to_owned(), + crate::Path::from_static("room"), Direction::Subscribe, route.consume(), ); @@ -2564,7 +2555,7 @@ mod tests { fn an_empty_token_never_holds_a_request() { let handle = Link::new(true); let _setup = handle.present(Bytes::new(), true).unwrap(); - let mut gate = Gate::new(handle, crate::Path::new("room").to_owned(), Direction::Subscribe); + let mut gate = Gate::new(handle, crate::Path::from_static("room"), Direction::Subscribe); assert!(gate.poll_ready(&kio::Waiter::noop()).is_ready()); } @@ -2574,7 +2565,7 @@ mod tests { let first = handle.present(Bytes::from_static(b"first"), false).unwrap(); handle.granted(first.presented.id, Grant::all()); let _unanswered = handle.present(Bytes::from_static(b"renewal"), false).unwrap(); - let mut gate = Gate::new(handle, crate::Path::new("room").to_owned(), Direction::Subscribe); + let mut gate = Gate::new(handle, crate::Path::from_static("room"), Direction::Subscribe); assert!(gate.poll_ready(&kio::Waiter::noop()).is_ready()); } @@ -2996,7 +2987,7 @@ mod tests { assert_eq!(link.retry(id, &lost), Some(RETRY_INITIAL)); assert_eq!(auth.grant().peek(), None, "the lost stream's grant is gone"); assert_eq!(link.token(&auth).as_deref(), Some(&b"jwt"[..])); - let mut gate = Gate::new(link.clone(), crate::Path::new("room").to_owned(), Direction::Publish); + let mut gate = Gate::new(link.clone(), crate::Path::from_static("room"), Direction::Publish); assert!(gate.poll_ready(&kio::Waiter::noop()).is_pending()); assert_eq!(link.reopen(id).unwrap().as_ref(), b"jwt"); @@ -3076,8 +3067,8 @@ mod request_token_tests { } } - fn subscribe_path() -> crate::PathOwned { - crate::Path::new("room/alice").to_owned() + fn subscribe_path() -> crate::Path { + crate::Path::from_static("room/alice") } /// A request token reaches the app's acceptor like any other token, and the grant the diff --git a/rs/moq-net/src/client.rs b/rs/moq-net/src/client.rs index f65f6edae12..75b13fe5721 100644 --- a/rs/moq-net/src/client.rs +++ b/rs/moq-net/src/client.rs @@ -1210,7 +1210,7 @@ mod tests { let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new()); let client = Client::new() .with_versions(Version::Lite(lite::Version::Lite04).into()) - .with_stats(registry.tier(Tier::default()).session("root")); + .with_stats(registry.tier(Tier::default()).session(crate::Path::from_static("root"))); let runtime = crate::time::Deadlines::new(crate::time::Instant::now()); let (session, mut driver) = futures::executor::block_on(client.connect(runtime.now(), fake)).unwrap(); session.abort(abort); @@ -1242,7 +1242,7 @@ mod tests { let registry = Registry::new(Default::default()); let client = Client::new() .with_versions(Version::Lite(lite::Version::Lite04).into()) - .with_stats(registry.tier(Tier::default()).session("root")); + .with_stats(registry.tier(Tier::default()).session(crate::Path::from_static("root"))); let runtime = crate::time::Deadlines::new(crate::time::Instant::now()); let mut task = Transport::new(FakeSession::new(Some(ALPN_LITE_04), Vec::new())); let (session, mut driver) = diff --git a/rs/moq-net/src/coding/reader.rs b/rs/moq-net/src/coding/reader.rs index dd4fc430321..59a64c59bcd 100644 --- a/rs/moq-net/src/coding/reader.rs +++ b/rs/moq-net/src/coding/reader.rs @@ -507,7 +507,7 @@ mod tests { stats::{Message, Registry, Tier, Totals}, }; let registry = Registry::new(Default::default()); - let session = registry.tier(Tier::default()).session(""); + let session = registry.tier(Tier::default()).session(crate::Path::from_static("")); let version = lite::Version::Lite07; let msg = lite::Probe { bitrate: Some(123), diff --git a/rs/moq-net/src/coding/writer.rs b/rs/moq-net/src/coding/writer.rs index 16101a1f222..272ebf0cdfd 100644 --- a/rs/moq-net/src/coding/writer.rs +++ b/rs/moq-net/src/coding/writer.rs @@ -416,7 +416,9 @@ mod tests { stats::{Message, Registry, Tier, Totals}, }; let stats = Registry::new(Default::default()); - let session = stats.tier(Tier::new("internal")).session(""); + let session = stats + .tier(Tier::new(crate::Path::from_static("internal"))) + .session(crate::Path::from_static("")); let gate = kio::Producer::new(false); let mut writer = Writer::new(SinkSend::gated(Log::default(), gate.consume()), lite::Version::Lite07).with_stats(&session); @@ -432,11 +434,15 @@ mod tests { assert!(writer.poll_flush(&mut Context::from_waker(Waker::noop())).is_pending()); assert_eq!( stats.snapshot().control(), - vec![(Tier::new("internal"), Message::Probe, Totals { tx: size, rx: 0 })] + vec![( + Tier::new(crate::Path::from_static("internal")), + Message::Probe, + Totals { tx: size, rx: 0 } + )] ); writer .buffer(&lite::Track { - broadcast: "test".into(), + broadcast: crate::Path::from_static("test"), epoch: None, track: "video".into(), }) diff --git a/rs/moq-net/src/discovery.rs b/rs/moq-net/src/discovery.rs index 299603420c8..25641014f31 100644 --- a/rs/moq-net/src/discovery.rs +++ b/rs/moq-net/src/discovery.rs @@ -15,7 +15,7 @@ use std::{ }; use crate::{ - Path, PathOwned, + Path, model::{InterestChange, Interests, Snapshot}, origin, time::{Deadline, Deadlines, Instant}, @@ -29,9 +29,9 @@ pub(crate) const LINGER: Duration = Duration::from_secs(2); pub(crate) enum Change { /// Ask the peer about the prefix, holding the snapshot until the request's /// initial set is in. - Open(PathOwned, Snapshot), + Open(Path, Snapshot), /// Stop asking about the prefix: end its request, retracting what it reported. - Close(PathOwned), + Close(Path), } /// The announce requests a session keeps open, as a covering set of what is wanted. @@ -52,14 +52,14 @@ pub(crate) struct Discovery { /// Decided but not yet handed to the session, in order. changes: VecDeque, /// How many wants name each prefix: overlapping scopes can name one twice. - wanted: HashMap, + wanted: HashMap, /// How many wanted prefixes sit at or beneath each prefix, so a request learns in a /// walk up its path whether anything beneath it is still wanted. - beneath: HashMap, + beneath: HashMap, /// The requests the session holds open, by prefix. - requests: HashMap, + requests: HashMap, /// The requests nothing beneath wants any more, by when they close. - lingering: BTreeSet<(Instant, PathOwned)>, + lingering: BTreeSet<(Instant, Path)>, /// Fires for the first of `lingering`. timer: Deadline, } @@ -69,7 +69,7 @@ struct Request { /// When it closes, while nothing at or beneath its prefix is wanted. linger: Option, /// Narrower requests nothing wants any more, kept open until this one goes. - held: HashSet, + held: HashSet, } impl Discovery { @@ -146,7 +146,7 @@ impl Discovery { } /// The request at `prefix` ended on its own: the peer finished it. - pub fn ended(&mut self, prefix: &PathOwned) { + pub fn ended(&mut self, prefix: &Path) { if let Some(request) = self.requests.remove(prefix) { self.forget(prefix, request); } @@ -209,7 +209,7 @@ impl Discovery { /// A new request at `prefix` replaces every request open beneath it, wanted or /// lingering, which closes first. - fn replace_beneath(&mut self, prefix: &PathOwned) -> Request { + fn replace_beneath(&mut self, prefix: &Path) -> Request { let beneath: Vec<_> = self .requests .keys() @@ -228,7 +228,7 @@ impl Discovery { /// A new request at `prefix` holds the narrower ones still lingering beneath it, as it /// would had they been released after it opened, so none closes before it has synced. - fn hold_lingering(&mut self, prefix: &PathOwned) -> Request { + fn hold_lingering(&mut self, prefix: &Path) -> Request { let mut request = Request::default(); let beneath: Vec<_> = self .lingering @@ -248,7 +248,7 @@ impl Discovery { } /// Keep the request at `prefix` open: something beneath it is wanted again. - fn stay(&mut self, prefix: &PathOwned) { + fn stay(&mut self, prefix: &Path) { let Some(request) = self.requests.get_mut(prefix) else { return; }; @@ -260,7 +260,7 @@ impl Discovery { /// Settle a request nothing beneath it wants any more. A broader open request that /// is still wanted holds it until that one goes; otherwise it lingers out. - fn release(&mut self, prefix: PathOwned) { + fn release(&mut self, prefix: Path) { for above in covering(&prefix).into_iter().skip(1) { if !self.beneath.contains_key(&above) { continue; @@ -283,7 +283,7 @@ impl Discovery { } /// Close a request that lingered out. - fn expire(&mut self, prefix: PathOwned) { + fn expire(&mut self, prefix: Path) { let Some(request) = self.requests.remove(&prefix) else { return; }; @@ -292,9 +292,9 @@ impl Discovery { } /// What a request that went held now settles on its own, unless it is wanted again. - fn forget(&mut self, prefix: &PathOwned, request: Request) { + fn forget(&mut self, prefix: &Path, request: Request) { if let Some(at) = request.linger { - self.lingering.remove(&(at, prefix.to_owned())); + self.lingering.remove(&(at, prefix.clone())); } for held in request.held { if self.requests.contains_key(&held) && !self.beneath.contains_key(&held) { @@ -305,10 +305,10 @@ impl Discovery { } /// `path` and every prefix above it, up to the root, narrowest first. -fn covering(path: &Path) -> Vec { +fn covering(path: &Path) -> Vec { std::iter::successors(Some(path.as_str()), |path| { (!path.is_empty()).then(|| path.rfind('/').map_or("", |slash| &path[..slash])) }) - .map(|path| Path::new(path).to_owned()) + .map(|prefix| Path::new(prefix).expect("a prefix of a path fits")) .collect() } diff --git a/rs/moq-net/src/error.rs b/rs/moq-net/src/error.rs index 7c6407ada23..d6aa0ccd520 100644 --- a/rs/moq-net/src/error.rs +++ b/rs/moq-net/src/error.rs @@ -616,6 +616,13 @@ impl From for Error { } } +/// Lets an entry point taking `impl TryInto>` accept a `Path` too. +impl From for Error { + fn from(never: std::convert::Infallible) -> Self { + match never {} + } +} + /// Which session code to send for a local error. /// /// Lossy on purpose: [`Error`] describes what went wrong locally, while the registry is what diff --git a/rs/moq-net/src/fuzz.rs b/rs/moq-net/src/fuzz.rs index f9dce155e8e..37629185218 100644 --- a/rs/moq-net/src/fuzz.rs +++ b/rs/moq-net/src/fuzz.rs @@ -13,7 +13,7 @@ use bytes::Bytes; use crate::{ - Hops, Path, PathOwned, Pattern, broadcast, cache, coding, + Hops, Path, Pattern, broadcast, cache, coding, coding::{Decode, Decoder, Encode, Encoder, Form, varint}, frame, group, ietf, lite, path::Relative, @@ -210,10 +210,10 @@ pub fn lite_wire(data: &[u8]) -> bool { match kind { 0 => roundtrip::(rest, version), 1 => roundtrip::(rest, version), - 2 => roundtrip::, _>(rest, version), - 3 => roundtrip::, _>(rest, version), + 2 => roundtrip::(rest, version), + 3 => roundtrip::(rest, version), 4 => roundtrip::(rest, version), - 5 => roundtrip::, _>(rest, version), + 5 => roundtrip::(rest, version), 6 => roundtrip::, _>(rest, version), 7 => roundtrip::(rest, version), 8 => roundtrip::(rest, version), @@ -230,7 +230,7 @@ pub fn lite_wire(data: &[u8]) -> bool { 19 => datagram(rest, version), 20 => roundtrip::(rest, version), 21 => roundtrip::(rest, version), - 22 => roundtrip::, _>(rest, version), + 22 => roundtrip::(rest, version), 23 => roundtrip::, _>(rest, version), 24 => roundtrip::(rest, version), 25 => roundtrip::(rest, version), @@ -244,7 +244,7 @@ pub fn lite_wire(data: &[u8]) -> bool { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Announced { /// ANNOUNCE_START: a route's suffix and wire hop chain, taking the next id. - Start(PathOwned, Hops), + Start(Path, Hops), /// ANNOUNCE_UPDATE: a live id's new wire hop chain. Update(u64, Hops), /// ANNOUNCE_END: a live id retired. @@ -323,7 +323,7 @@ pub fn excluding(origin: crate::origin::Consumer, peer: crate::Hop) -> crate::or /// Land a claim with a peer's wire route, as a session does, for the origin bench. pub fn requests_route( origin: &crate::origin::Producer, - prefix: impl crate::AsPath, + prefix: impl Into, route: crate::origin::Route, ) -> Result { origin.requests_route(prefix, route) @@ -374,7 +374,7 @@ impl Interests { /// drops the entry unless some guard still holds it. Not a public API. pub fn bump_publisher_bytes( registry: &crate::stats::Registry, - path: impl crate::AsPath, + path: impl Into, tier: &crate::stats::Tier, n: u64, ) { @@ -411,13 +411,13 @@ impl Meter { /// Build a session-local payload meter without exposing the model's private stats carriers. pub fn session_meter(registry: &crate::stats::Registry, tier: crate::stats::Tier, label: &str, path: &str) -> Meter { - let session = registry.tier(tier).session(label); - Meter(session.egress(path).meter()) + let session = registry.tier(tier).session(Path::new(label).expect("label fits")); + Meter(session.egress(Path::new(path).expect("path fits")).meter()) } /// Build the original per-path-only payload meter as the benchmark baseline. pub fn path_meter(registry: &crate::stats::Registry, tier: &crate::stats::Tier, path: &str) -> Meter { - Meter(registry.publisher_meter(path, tier)) + Meter(registry.publisher_meter(Path::new(path).expect("path fits"), tier)) } /// Encode `announced` as a fresh [`AnnounceWriter`] stream. @@ -457,6 +457,30 @@ pub fn decode_announces(data: &[u8], compress: bool) -> Vec { resolved } +/// Encode a moq-transport NAMESPACE message for each of `paths`. +pub fn encode_namespaces(paths: &[Path]) -> Vec { + let mut data = Vec::new(); + let w = &mut Encoder::new(&mut data, BENCH_IETF.into()); + for path in paths { + let msg = ietf::Namespace { + suffix: path.clone(), + params: None, + }; + msg.encode(w, BENCH_IETF).expect("a namespace that fits"); + } + data +} + +/// Decode the NAMESPACE messages [`encode_namespaces`] wrote. +pub fn decode_namespaces(data: &[u8]) -> Vec { + let r = &mut Decoder::new(data, BENCH_IETF.into()); + let mut paths = Vec::new(); + while !r.is_empty() { + paths.push(ietf::Namespace::decode(r, BENCH_IETF).expect("our own encoding").suffix); + } + paths +} + /// A varint-heavy lite wire object, as the varint bench times it: what a publisher /// pays per frame, per group, and per request. #[derive(Clone, Copy, Debug)] @@ -525,7 +549,7 @@ impl LiteSample { order: crate::track::Order::Descending, epoch: None, id: 3, - broadcast: Path::new("room/alice"), + broadcast: Path::from_static("room/alice"), track: "video".into(), priority: 2, max_delay: std::time::Duration::from_secs(10), @@ -615,10 +639,10 @@ pub fn ietf_wire(data: &[u8]) -> bool { 4 => roundtrip::(rest, version), 5 => roundtrip::(rest, version), 6 => roundtrip::, _>(rest, version), - 7 => roundtrip::, _>(rest, version), + 7 => roundtrip::(rest, version), 8 => roundtrip::(rest, version), 9 => roundtrip::, _>(rest, version), - 10 => roundtrip::, _>(rest, version), + 10 => roundtrip::(rest, version), 11 => roundtrip::, _>(rest, version), 12 => roundtrip::, _>(rest, version), 13 => roundtrip::, _>(rest, version), @@ -631,13 +655,13 @@ pub fn ietf_wire(data: &[u8]) -> bool { 20 => roundtrip::, _>(rest, version), 21 => roundtrip::(rest, version), 22 => roundtrip::(rest, version), - 23 => roundtrip::, _>(rest, version), - 24 => 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), + 27 => roundtrip::(rest, version), + 28 => roundtrip::(rest, version), + 29 => roundtrip::(rest, version), 30 => roundtrip::(rest, version), 31 => roundtrip::(rest, version), 32 => roundtrip::(rest, version), @@ -648,7 +672,7 @@ pub fn ietf_wire(data: &[u8]) -> bool { 37 => roundtrip::(rest, version), 38 => roundtrip::(rest, version), 39 => roundtrip::(rest, version), - 40 => 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!( @@ -750,7 +774,7 @@ impl Default for Messages { order: crate::track::Order::Descending, epoch: None, id: 7, - broadcast: Path::new("room/alice"), + broadcast: Path::from_static("room/alice"), track: "video".into(), priority: 3, max_delay: Duration::from_millis(500), @@ -789,7 +813,7 @@ impl Default for Messages { max_age: None, new_group: None, request_id: ietf::RequestId(2), - track_namespace: Path::new("room/alice"), + track_namespace: Path::from_static("room/alice"), track_name: "video".into(), subscriber_priority: 128, group_order: ietf::GroupOrder::Descending, @@ -902,7 +926,7 @@ pub fn decode_varints(data: &[u8], ietf: bool) -> u64 { /// Decode one broadcast path as moq-lite carries it (`ietf == false`), a string, or as /// a moq-transport namespace tuple. -pub fn decode_path(data: &[u8], ietf: bool) -> PathOwned { +pub fn decode_path(data: &[u8], ietf: bool) -> Path { let r = &mut Decoder::new(data, bench_form(ietf)); let path = match ietf { false => Path::decode(r, BENCH_LITE).unwrap(), @@ -923,14 +947,29 @@ pub fn path(data: &[u8]) -> bool { }; let (target, base) = text.split_once('\n').unwrap_or((text, "")); - let target = Path::new(target); - let base = Path::new(base); + // Only a path over the limit is refused, and the limit applies after normalizing. + let (Ok(target), Ok(base)) = (Path::new(target), Path::new(base)) else { + let normalized = |s: &str| { + s.split('/') + .filter(|part| !part.is_empty()) + .collect::>() + .join("/") + .len() + }; + assert!( + normalized(target).max(normalized(base)) > Path::MAX_BYTES, + "a path within the limit was refused" + ); + return true; + }; // Normalization is a fixed point: re-parsing what `as_str` printed changes nothing, // or every path that made a round trip through the wire would drift. - assert_eq!(Path::new(target.as_str()), target, "normalization is not idempotent"); - assert_eq!(target.to_owned(), target, "owning a path changed it"); - assert_eq!(target.borrow(), target, "borrowing a path changed it"); + assert_eq!( + Path::new(target.as_str()), + Ok(target.clone()), + "normalization is not idempotent" + ); assert_eq!(target.parts().collect::>().join("/"), target.as_str()); // A prefix is a prefix whichever way you ask. @@ -940,20 +979,30 @@ pub fn path(data: &[u8]) -> bool { "has_prefix and strip_prefix disagree" ); if let Some(rest) = target.strip_prefix(&base) { - assert_eq!(base.join(&rest), target, "strip_prefix did not invert join"); + assert_eq!(base.join(&rest), Ok(target.clone()), "strip_prefix did not invert join"); } - // Joining descends, so the result is always under what it started from. - let joined = target.join(&base); - assert!(joined.has_prefix(&target), "join escaped its base"); + // Joining descends, so the result is always under what it started from, and fails + // only past the limit. + match target.join(&base) { + Ok(joined) => assert!(joined.has_prefix(&target), "join escaped its base"), + Err(_) => assert!( + target.len() + 1 + base.len() > Path::MAX_BYTES, + "a join within the limit failed" + ), + } // `relative` is documented as the inverse of `resolve`, and as never walking above // the root, so `try_resolve` has to accept whatever it produces. if let Some(rel) = target.relative(&base) { - assert_eq!(base.resolve(&rel), target, "relative did not invert resolve"); + assert_eq!( + base.resolve(&rel), + Ok(target.clone()), + "relative did not invert resolve" + ); assert_eq!( base.try_resolve(&rel), - Some(target.to_owned()), + Some(target.clone()), "relative escaped the root" ); } @@ -961,9 +1010,9 @@ pub fn path(data: &[u8]) -> bool { // Resolving arbitrary references must stay inside the clamped/unclamped contract: // `try_resolve` only refuses by walking above the root, so whenever it answers, it // answers the same as `resolve`. - let rel = Relative::new(base.as_str()); + let rel = Relative::new(base.as_str()).expect("a reference no longer than its path fits"); if let Some(resolved) = target.try_resolve(&rel) { - assert_eq!(resolved, target.resolve(&rel), "try_resolve disagreed with resolve"); + assert_eq!(Ok(resolved), target.resolve(&rel), "try_resolve disagreed with resolve"); } // The wire form is the same path back, whenever the path is expressible at all. @@ -1173,7 +1222,7 @@ pub fn seeds() -> Vec { group_id: 0, }, ietf::FetchType::Filtered { - namespace: crate::Path::new("a"), + namespace: crate::Path::from_static("a"), track: "b".into(), filter: ietf::Filter::Relative(1), }, @@ -1236,7 +1285,7 @@ pub fn seeds() -> Vec { let mut announced = Vec::new(); let hop = |id: u64| crate::Hop::new(id).expect("a valid hop"); for n in 0..12u64 { - let suffix = PathOwned::from(format!("pid{}/private/channel_{}/health-{n}", n % 3, n % 2)); + let suffix = Path::new(&format!("pid{}/private/channel_{}/health-{n}", n % 3, n % 2)).unwrap(); let hops = Hops::try_from(vec![hop(100 + n % 3), hop(1 << 40), hop(1 << 41)]).expect("valid hops"); announced.push(Announced::Start(suffix, hops)); if n % 4 == 3 { @@ -1496,13 +1545,13 @@ mod tests { let registry = crate::stats::Registry::new(crate::stats::Config::new()); let (origin, driver) = crate::origin::Producer::new(crate::origin::Config::default()); let _driver = driver; - let tier = crate::stats::Tier::new("t"); - let session = registry.tier(tier.clone()).session("root"); + let tier = crate::stats::Tier::new(crate::Path::from_static("t")); + let session = registry.tier(tier.clone()).session(crate::Path::from_static("root")); let _broadcast = origin.publish("room/1", crate::origin::Route::default()).unwrap(); let mut cursor = origin.consume().with_stats(session).announced(); while cursor.next().now_or_never().flatten().is_some() {} - bump_publisher_bytes(®istry, "room/1", &tier, 5); + bump_publisher_bytes(®istry, crate::Path::from_static("room/1"), &tier, 5); let mut report = crate::stats::Report::default(); registry.report(&mut report); registry.report(&mut report); diff --git a/rs/moq-net/src/ietf/adapter.rs b/rs/moq-net/src/ietf/adapter.rs index 1c27f798016..43a02812e08 100644 --- a/rs/moq-net/src/ietf/adapter.rs +++ b/rs/moq-net/src/ietf/adapter.rs @@ -9,7 +9,7 @@ use std::{ use bytes::{Buf, Bytes, BytesMut}; use crate::{ - Error, PathOwned, + Error, Path, coding::{Decode, Decoder, Encoder, Reader, Writer}, ietf::{self, RequestId}, }; @@ -645,9 +645,9 @@ struct Namespaces { #[derive(Default)] struct NamespacesState { - outgoing: HashMap, - incoming: HashMap, - subscribed: HashMap, + 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 @@ -663,13 +663,13 @@ struct NamespaceRequests { struct NamespaceRegistration { direction: Direction, - namespace: PathOwned, + namespace: Path, previous: Option, next: Option, } impl NamespacesState { - fn map(&mut self, direction: Direction) -> &mut HashMap { + fn map(&mut self, direction: Direction) -> &mut HashMap { match direction { Direction::Outgoing => &mut self.outgoing, Direction::Incoming => &mut self.incoming, @@ -685,7 +685,7 @@ impl Namespaces { /// 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) { + fn register(&self, direction: Direction, namespace: Path, request_id: RequestId) { let mut state = self.state.lock().unwrap(); if state.by_request.contains_key(&request_id) { return; @@ -717,13 +717,13 @@ impl Namespaces { } /// The namespace the peer's request `request_id` advertises, if it is a PUBLISH_NAMESPACE. - fn incoming(&self, request_id: RequestId) -> Option { + 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 { + fn get(&self, direction: Direction, namespace: &Path) -> Option { self.state .lock() .unwrap() @@ -796,9 +796,9 @@ struct Shared { /// its stream, which it does after releasing the route. Bounded by the peer's requests. #[derive(Default)] struct Readvertised { - namespaces: HashMap, + namespaces: HashMap, /// Each withdrawn request a handler still holds, to the namespace it advertised. - withdrawn: HashMap, + withdrawn: HashMap, } #[derive(Default)] @@ -1242,7 +1242,7 @@ fn classify(type_id: u64, body: &Bytes, version: Version, namespaces: &Namespace 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); + namespaces.register(Direction::Subscribed, prefix, id); } Ok(Route::NewRequest(id)) } @@ -1617,7 +1617,7 @@ fn decode_response_request_id(body: &Bytes, version: Version) -> Result Result { +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)?; @@ -1626,7 +1626,7 @@ fn decode_publish_namespace_body(body: &Bytes, version: Version) -> Result ietf::PublishNamespace<'_> { + fn publish_namespace(request_id: RequestId, namespace: &str) -> ietf::PublishNamespace { ietf::PublishNamespace { request_id, - track_namespace: crate::Path::new(namespace), + track_namespace: crate::Path::new(namespace).unwrap(), params: Default::default(), authorization_token: None, } @@ -2071,11 +2071,11 @@ mod tests { #[test] fn test_namespace_reverse_lookup_v14() { let namespaces = Namespaces::default(); - namespaces.register(Direction::Incoming, crate::Path::new("test/ns"), RequestId(42)); + namespaces.register(Direction::Incoming, crate::Path::from_static("test/ns"), RequestId(42)); let body = encode_body( &ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("test/ns"), + track_namespace: crate::Path::from_static("test/ns"), request_id: RequestId(0), }, Version::Draft14, @@ -2093,7 +2093,7 @@ mod tests { let (shared, mut ours, mut theirs) = mesh("cluster/ns", version, Direction::Outgoing).await; let done = ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("cluster/ns"), + track_namespace: crate::Path::from_static("cluster/ns"), request_id: RequestId(0), }; let route = classify( @@ -2119,7 +2119,7 @@ mod tests { let (shared, mut ours, mut theirs) = mesh("cluster/ns", version, Direction::Incoming).await; let cancel = ietf::PublishNamespaceCancel { - track_namespace: crate::Path::new("cluster/ns"), + track_namespace: crate::Path::from_static("cluster/ns"), request_id: RequestId(0), error_code: 0, reason_phrase: "".into(), @@ -2142,7 +2142,7 @@ mod tests { /// 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), + track_namespace: crate::Path::new(namespace).unwrap(), request_id: RequestId(0), }; classify( @@ -2157,7 +2157,7 @@ mod tests { /// 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), + track_namespace: crate::Path::new(namespace).unwrap(), request_id: RequestId(0), error_code: 0, reason_phrase: "".into(), @@ -2205,7 +2205,7 @@ mod tests { let mut theirs = receive(&shared, "cluster/ns", version, RequestId(7)).await; let msg = ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("cluster/ns"), + track_namespace: crate::Path::from_static("cluster/ns"), request_id: RequestId(0), }; shared.close(RequestId(7), encode_msg(&msg, version)); @@ -2239,7 +2239,7 @@ mod tests { /// 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), + track_namespace: crate::Path::new(namespace).unwrap(), request_id, }; let route = classify( @@ -2363,7 +2363,7 @@ mod tests { } let fourth = (0..4).find(|id| *id != first && *id != second && *id != third).unwrap(); let namespaces = Namespaces::default(); - let name = crate::Path::new("ns"); + let name = crate::Path::from_static("ns"); for id in 0..4 { namespaces.register(direction, name.clone(), RequestId(id)); } @@ -2432,7 +2432,7 @@ mod tests { }; for version in [Version::Draft14, Version::Draft15, Version::Draft16] { let registry = Registry::new(Default::default()); - let stats = registry.tier(Tier::default()).session(""); + let stats = registry.tier(Tier::default()).session(crate::Path::from_static("")); let msg = ietf::FetchCancel { request_id: RequestId(0), }; diff --git a/rs/moq-net/src/ietf/fetch.rs b/rs/moq-net/src/ietf/fetch.rs index c47fd893a4c..4df2cb34974 100644 --- a/rs/moq-net/src/ietf/fetch.rs +++ b/rs/moq-net/src/ietf/fetch.rs @@ -22,7 +22,7 @@ use super::Version; pub enum FetchType<'a> { /// An inclusive range of a track, through draft-18. Standalone { - namespace: Path<'a>, + namespace: Path, track: Cow<'a, str>, start: Location, end: Location, @@ -38,7 +38,7 @@ pub enum FetchType<'a> { /// A track, with the range as a LOCATION_FILTER, from draft-22 on. Unfiltered is /// everything from `{0, 0}` up to Largest Object. Filtered { - namespace: Path<'a>, + namespace: Path, track: Cow<'a, str>, filter: Filter, }, @@ -702,7 +702,7 @@ mod tests { subscriber_priority: 128, group_order: GroupOrder::Descending, fetch_type: FetchType::Standalone { - namespace: Path::new("test"), + namespace: Path::from_static("test"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 10, object: 5 }, @@ -727,7 +727,7 @@ mod tests { subscriber_priority: 128, group_order: GroupOrder::Descending, fetch_type: FetchType::Standalone { - namespace: Path::new("test"), + namespace: Path::from_static("test"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 10, object: 5 }, @@ -770,7 +770,7 @@ mod tests { subscriber_priority: 128, group_order: GroupOrder::Descending, fetch_type: FetchType::Standalone { - namespace: Path::new("test"), + namespace: Path::from_static("test"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 10, object: 5 }, @@ -795,7 +795,7 @@ mod tests { subscriber_priority: 128, group_order: GroupOrder::Descending, fetch_type: FetchType::Standalone { - namespace: Path::new("test"), + namespace: Path::from_static("test"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 10, object: 5 }, @@ -874,7 +874,7 @@ mod tests { subscriber_priority: 128, group_order: GroupOrder::Descending, fetch_type: FetchType::Standalone { - namespace: Path::new("test"), + namespace: Path::from_static("test"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 10, object: 5 }, @@ -967,7 +967,7 @@ mod tests { assert_eq!( fetch.fetch_type, FetchType::Filtered { - namespace: Path::new("live"), + namespace: Path::from_static("live"), track: "video".into(), filter: Filter::Absolute { start: Location { group: 128, object: 0 }, @@ -989,7 +989,7 @@ mod tests { subscriber_priority: 128, group_order: GroupOrder::Descending, fetch_type: FetchType::Filtered { - namespace: Path::new("live"), + namespace: Path::from_static("live"), track: "video".into(), filter: Filter::Unfiltered, }, diff --git a/rs/moq-net/src/ietf/namespace.rs b/rs/moq-net/src/ietf/namespace.rs index 1a7bee0e284..686d5373489 100644 --- a/rs/moq-net/src/ietf/namespace.rs +++ b/rs/moq-net/src/ietf/namespace.rs @@ -1,4 +1,4 @@ -use crate::{Path, coding::*}; +use crate::{Path, coding::*, path::TooLong}; fn to_tuple(namespace: &Path) -> Vec { let path = namespace.as_str(); @@ -28,7 +28,7 @@ fn to_tuple(namespace: &Path) -> Vec { parts } -fn from_tuple(parts: &[&str]) -> Path<'static> { +fn from_tuple(parts: &[&str]) -> Result { // Sized for the common case of no escapes, so building it rarely grows. let mut path = String::with_capacity(parts.iter().map(|part| part.len() + 1).sum()); for (i, part) in parts.iter().enumerate() { @@ -70,11 +70,11 @@ const MAX_BYTES: usize = 4096; /// /// Every limit here closes the session as a PROTOCOL_VIOLATION (draft-22 section 8.7): /// more than 32 fields, an empty field, or more than 4096 bytes. -fn decode_fields(r: &mut Decoder<'_>) -> Result<(Path<'static>, usize), DecodeError> { +fn decode_fields(r: &mut Decoder<'_>) -> Result<(Path, usize), DecodeError> { let count = r.varint()?; if count == 0 { - return Ok((Path::from(String::new()), 0)); + return Ok((Path::empty(), 0)); } if count > Path::MAX_PARTS as u64 { @@ -96,16 +96,16 @@ fn decode_fields(r: &mut Decoder<'_>) -> Result<(Path<'static>, usize), DecodeEr } } - Ok((from_tuple(parts), size)) + Ok((from_tuple(parts)?, size)) } /// Decode a Track Namespace. -pub fn decode_namespace(r: &mut Decoder<'_>) -> Result, DecodeError> { +pub fn decode_namespace(r: &mut Decoder<'_>) -> Result { Ok(decode_fields(r)?.0) } /// Decode a Full Track Name: a Track Namespace, then a Track Name, at most 4096 bytes together. -pub fn decode_track(r: &mut Decoder<'_>) -> Result<(Path<'static>, String), DecodeError> { +pub fn decode_track(r: &mut Decoder<'_>) -> Result<(Path, String), DecodeError> { let (namespace, size) = decode_fields(r)?; let name = r.string()?; if size + name.len() > MAX_BYTES { @@ -159,7 +159,7 @@ mod tests { (vec![".application"], false), (vec![".sessionish"], false), ] { - let path = from_tuple(&fields); + let path = from_tuple(&fields).unwrap(); assert_eq!( refusal(&path, super::super::Version::Draft22).is_some(), reserved, @@ -173,16 +173,16 @@ mod tests { const FORM: Form = Form::LeadingOnes { seven: false }; fn encode_ns(path: &str) -> Vec { - encode_path(&Path::from(path.to_string())) + encode_path(&crate::Path::try_from(path.to_string()).unwrap()) } - fn encode_path(path: &Path<'_>) -> Vec { + fn encode_path(path: &Path) -> Vec { let mut buf = Vec::new(); encode_namespace(&mut Encoder::new(&mut buf, FORM), path).unwrap(); buf } - fn decode_ns(bytes: &[u8]) -> Path<'static> { + fn decode_ns(bytes: &[u8]) -> Path { decode_namespace(&mut Decoder::new(bytes, FORM)).unwrap() } diff --git a/rs/moq-net/src/ietf/publish.rs b/rs/moq-net/src/ietf/publish.rs index 55a6dcd1851..820b6987816 100644 --- a/rs/moq-net/src/ietf/publish.rs +++ b/rs/moq-net/src/ietf/publish.rs @@ -223,7 +223,7 @@ impl Message for PublishDone<'_> { #[derive(Debug)] pub struct Publish<'a> { pub request_id: RequestId, - pub track_namespace: Path<'a>, + pub track_namespace: Path, pub track_name: Cow<'a, str>, pub track_alias: u64, pub largest_location: Option, @@ -545,7 +545,7 @@ mod tests { let mut body = Vec::new(); let w = &mut Encoder::new(&mut body, version.into()); RequestId(1).encode(w, version)?; - super::super::namespace::encode_namespace(w, &crate::Path::new("broadcast"))?; + super::super::namespace::encode_namespace(w, &crate::Path::from_static("broadcast"))?; w.string("video")?; w.varint(1u64)?; // track alias @@ -568,7 +568,7 @@ mod tests { let mut body = Vec::new(); let w = &mut Encoder::new(&mut body, version.into()); RequestId(1).encode(w, version)?; - super::super::namespace::encode_namespace(w, &crate::Path::new("broadcast"))?; + super::super::namespace::encode_namespace(w, &crate::Path::from_static("broadcast"))?; w.string("video")?; w.varint(1u64)?; encode_params!(w, version, 0x20 => 128u8); @@ -594,7 +594,7 @@ mod tests { fn test_publish_v14_round_trip() { let msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), track_alias: 42, largest_location: Some(Location { group: 10, object: 5 }), @@ -623,7 +623,7 @@ mod tests { fn test_publish_v15_round_trip() { let msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), track_alias: 42, largest_location: Some(Location { group: 10, object: 5 }), @@ -690,7 +690,7 @@ mod tests { fn test_publish_v17_round_trip() { let msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), track_alias: 42, largest_location: Some(Location { group: 10, object: 5 }), @@ -741,7 +741,7 @@ mod tests { if version == Version::Draft17 { w.varint(0).unwrap(); } - encode_namespace(w, &Path::new("t")).unwrap(); + encode_namespace(w, &Path::from_static("t")).unwrap(); w.string("v").unwrap(); w.varint(1).unwrap(); w.varint(1).unwrap(); @@ -853,7 +853,7 @@ mod tests { fn test_publish_v18_round_trip() { let msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), track_alias: 42, largest_location: Some(Location { group: 10, object: 5 }), @@ -890,7 +890,7 @@ mod tests { ] { let mut msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("ns"), + track_namespace: Path::from_static("ns"), track_name: "video".into(), track_alias: 42, largest_location: None, @@ -926,7 +926,7 @@ mod tests { fn test_publish_v18_group_order_is_a_property() { let msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("ns"), + track_namespace: Path::from_static("ns"), track_name: "video".into(), track_alias: 42, largest_location: None, @@ -968,7 +968,7 @@ mod tests { fn test_publish_v15_group_order_is_a_parameter() { let msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("ns"), + track_namespace: Path::from_static("ns"), track_name: "video".into(), track_alias: 42, largest_location: None, @@ -1010,7 +1010,7 @@ mod tests { fn test_publish_v18_is_one_byte_shorter_than_v17() { let msg = Publish { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), track_alias: 42, largest_location: None, diff --git a/rs/moq-net/src/ietf/publish_namespace.rs b/rs/moq-net/src/ietf/publish_namespace.rs index 6643182bd19..03e22cb0485 100644 --- a/rs/moq-net/src/ietf/publish_namespace.rs +++ b/rs/moq-net/src/ietf/publish_namespace.rs @@ -43,9 +43,9 @@ pub struct AdvertParams { /// PublishNamespace message (0x06) /// Sent by the publisher to announce the availability of a namespace. #[derive(Clone, Debug)] -pub struct PublishNamespace<'a> { +pub struct PublishNamespace { pub request_id: RequestId, - pub track_namespace: Path<'a>, + pub track_namespace: Path, /// The extension parameters, which a session that negotiated none leaves empty. pub params: AdvertParams, @@ -55,7 +55,7 @@ pub struct PublishNamespace<'a> { pub authorization_token: Option, } -impl PublishNamespace<'_> { +impl PublishNamespace { /// Decode the message body, expecting the parameters of the extensions the session /// negotiated. /// @@ -92,7 +92,7 @@ impl PublishNamespace<'_> { } } -impl Message for PublishNamespace<'_> { +impl Message for PublishNamespace { const ID: u64 = 0x06; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -330,14 +330,14 @@ impl Message for PublishNamespaceError<'_> { /// PublishNamespaceDone message (0x09) /// v14/v15: uses track_namespace. v16: uses request_id. #[derive(Clone, Debug)] -pub struct PublishNamespaceDone<'a> { +pub struct PublishNamespaceDone { /// v14/v15: the namespace being unannounced - pub track_namespace: Path<'a>, + pub track_namespace: Path, /// v16: the request ID of the original PublishNamespace pub request_id: RequestId, } -impl Message for PublishNamespaceDone<'_> { +impl Message for PublishNamespaceDone { const ID: u64 = 0x09; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -379,7 +379,7 @@ impl Message for PublishNamespaceDone<'_> { #[derive(Clone, Debug)] pub struct PublishNamespaceCancel<'a> { /// v14/v15: the namespace being cancelled - pub track_namespace: Path<'a>, + pub track_namespace: Path, /// v16: the request ID of the original PublishNamespace pub request_id: RequestId, pub error_code: u64, @@ -451,7 +451,7 @@ mod tests { fn test_announce_round_trip() { let msg = PublishNamespace { request_id: RequestId(1), - track_namespace: Path::new("test/broadcast"), + track_namespace: Path::from_static("test/broadcast"), params: Default::default(), authorization_token: None, }; @@ -480,7 +480,7 @@ mod tests { #[test] fn test_unannounce_v14() { let msg = PublishNamespaceDone { - track_namespace: Path::new("old/stream"), + track_namespace: Path::from_static("old/stream"), request_id: RequestId(0), }; @@ -506,7 +506,7 @@ mod tests { #[test] fn test_announce_cancel_v14() { let msg = PublishNamespaceCancel { - track_namespace: Path::new("canceled"), + track_namespace: Path::from_static("canceled"), request_id: RequestId(0), error_code: 1, reason_phrase: "Shutdown".into(), @@ -541,7 +541,7 @@ mod tests { fn test_publish_namespace_v17_round_trip() { let msg = PublishNamespace { request_id: RequestId(5), - track_namespace: Path::new("v17/broadcast"), + track_namespace: Path::from_static("v17/broadcast"), params: Default::default(), authorization_token: None, }; @@ -557,7 +557,7 @@ mod tests { fn test_publish_namespace_v18_round_trip() { let msg = PublishNamespace { request_id: RequestId(5), - track_namespace: Path::new("v18/broadcast"), + track_namespace: Path::from_static("v18/broadcast"), params: Default::default(), authorization_token: None, }; diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index 411f9cb7726..fac8175b32c 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -17,7 +17,7 @@ use crate::transport::poll::SendGroup as _; use crate::transport::poll::{GroupOf, SendOptions}; use crate::{ - AsPath, DecodeError, Error, Timescale, Timestamp, + DecodeError, Error, Timescale, Timestamp, coding::{Encode as _, Encoder, Stream, Writer}, ietf::{self, Control, EndLocation, FetchHeader, FetchType, Filter, GroupOrder, Location, RequestId}, priority::{Priority, PriorityHandle, PriorityQueue}, @@ -681,18 +681,18 @@ struct Namespaces { peer: cluster::Peer, target: Target, /// Every announced broadcast under this loop's prefix. - watched: HashMap, + watched: HashMap, /// The open PUBLISH_NAMESPACE request carrying each advertised namespace. Empty when /// the entries ride a SUBSCRIBE_NAMESPACE stream inline. - requests: HashMap, + requests: HashMap, /// Requests with an update to send, in the order they asked for a request ID. Each /// waits for credit beside the loop's other work and computes its update when it gets /// one, so a later change supersedes an earlier one that is still waiting. - updates: VecDeque, + updates: VecDeque, /// Requests with an update in flight, each answered on its own stream, by when each /// gives up. Sent in order with one timeout, so the front is the earliest; an entry /// whose request moved on is skipped when it reaches the front. - inflight: VecDeque<(crate::time::Instant, crate::PathOwned)>, + inflight: VecDeque<(crate::time::Instant, crate::Path)>, /// Requests whose stream woke the loop, so a wake polls those rather than every /// update in flight. woken: Arc, @@ -819,7 +819,7 @@ enum NamespaceEvent { /// The queued update at the front got this request ID. Renew(RequestId), /// What came back for an update in flight. - Answered(crate::PathOwned, Result), + Answered(crate::Path, Result), } /// What came back for a PUBLISH_NAMESPACE or an update in flight. @@ -835,14 +835,14 @@ enum Answer { /// The requests whose stream woke an announce loop since it last looked. #[derive(Default)] struct Woken { - suffixes: std::sync::Mutex>, + suffixes: std::sync::Mutex>, /// The loop, woken in turn. owner: std::sync::Mutex>, } impl Woken { /// A waker that marks `suffix` woken. - fn waker(self: &Arc, suffix: crate::PathOwned) -> std::task::Waker { + fn waker(self: &Arc, suffix: crate::Path) -> std::task::Waker { std::task::Waker::from(Arc::new(RequestWaker { woken: self.clone(), suffix, @@ -852,7 +852,7 @@ impl Woken { struct RequestWaker { woken: Arc, - suffix: crate::PathOwned, + suffix: crate::Path, } impl std::task::Wake for RequestWaker { @@ -930,7 +930,7 @@ enum Joined { end: Location, cache: track::Consumer, /// The subscription's namespace, which the fetch is held to the grant on. - namespace: crate::PathOwned, + namespace: crate::Path, /// The subscription's own request token authorized it, so the limit does not bound /// the fetch either. token: bool, @@ -1127,7 +1127,7 @@ impl Publisher { tracing::debug!(message = ?msg, "received subscribe"); let gate = crate::auth::Gate::new( this.auth.clone(), - msg.track_namespace.to_owned(), + msg.track_namespace.clone(), crate::auth::Direction::Publish, ); let task = this.serve_subscribe_stream(stream, msg, gate); @@ -1210,7 +1210,7 @@ impl Publisher { tracing::debug!(message = ?msg, "received subscribe_namespace"); let gate = crate::auth::Gate::new( this.auth.clone(), - msg.namespace.to_owned(), + msg.namespace.clone(), crate::auth::Direction::Publish, ); async move { @@ -1270,7 +1270,7 @@ impl Publisher { tracing::debug!(message = ?msg, "received track_status"); let gate = crate::auth::Gate::new( this.auth.clone(), - msg.track_namespace.to_owned(), + msg.track_namespace.clone(), crate::auth::Direction::Publish, ); async move { @@ -1317,7 +1317,7 @@ impl Publisher { ) -> impl std::future::Future> { let gate = crate::auth::Gate::new( self.auth.clone(), - msg.track_namespace.to_owned(), + msg.track_namespace.clone(), crate::auth::Direction::Publish, ); self.serve_subscribe_stream(stream, msg, gate) @@ -1358,9 +1358,11 @@ impl Publisher { }; let request_id = msg.request_id; let track_name = msg.track_name.clone(); - let absolute = self.origin.absolute(&msg.track_namespace).to_owned(); - - tracing::debug!(id = %request_id, broadcast = %absolute, track = %track_name, "subscribe started"); + // Logged beside the root rather than joined, so a namespace too long under it is + // refused by the lookup, on this request alone. + let (root, namespace) = (self.origin.root().clone(), msg.track_namespace.clone()); + let broadcast = || crate::path::Rooted(&root, &namespace); + tracing::debug!(id = %request_id, broadcast = %broadcast(), track = %track_name, "subscribe started"); // Serve only what our grant lets us publish (MoQ Auth), and stop once it no // longer does. Checked before resolving, so a denied request never reaches the @@ -1429,7 +1431,7 @@ impl Publisher { &self.runtime, verdict, verified, - msg.track_namespace.to_owned(), + msg.track_namespace.clone(), crate::auth::RequestKind::Subscribe, Some(slot.clone()), ); @@ -1523,7 +1525,7 @@ impl Publisher { // the same boundary used by the subscription so the two streams never overlap, // again on every resume. let filter = msg.filter; - let namespace = msg.track_namespace.to_owned(); + let namespace = msg.track_namespace.clone(); let token = request_grant.is_some(); let save_join = |largest: Option, cache: &crate::track::Consumer, @@ -1693,11 +1695,11 @@ impl Publisher { { // PUBLISH_DONE carries a fixed reason per status, so only the code reaches // the peer once SUBSCRIBE_OK went out. - tracing::info!(broadcast = %absolute, track = %track_name, err = %refusal.error, reason = %refusal.reason, "request token grant ended"); + tracing::info!(broadcast = %broadcast(), track = %track_name, err = %refusal.error, reason = %refusal.reason, "request token grant ended"); return Poll::Ready(refusal); } if gate.poll_denied(waiter).is_ready() { - tracing::info!(broadcast = %absolute, track = %track_name, "subscription no longer authorized"); + tracing::info!(broadcast = %broadcast(), track = %track_name, "subscription no longer authorized"); return Poll::Ready(Error::Unauthorized.into()); } Poll::Pending @@ -2964,7 +2966,7 @@ impl Publisher { fn fetch_gate(&self, msg: &ietf::Fetch<'_>) -> FetchGate { match &msg.fetch_type { FetchType::Standalone { namespace, .. } | FetchType::Filtered { namespace, .. } => FetchGate::Standalone( - crate::auth::Gate::new(self.auth.clone(), namespace.to_owned(), crate::auth::Direction::Publish), + crate::auth::Gate::new(self.auth.clone(), namespace.clone(), crate::auth::Direction::Publish), ), FetchType::RelativeJoining { subscriber_request_id, .. @@ -3452,7 +3454,7 @@ impl Publisher { ( Location, track::Consumer, - crate::PathOwned, + crate::Path, Ranking>, bool, ), @@ -3540,7 +3542,7 @@ impl Publisher { ) -> impl std::future::Future> { let gate = crate::auth::Gate::new( self.auth.clone(), - msg.track_namespace.to_owned(), + msg.track_namespace.clone(), crate::auth::Direction::Publish, ); self.serve_track_status_stream(stream, msg, gate) @@ -3695,12 +3697,7 @@ impl Publisher { /// already carries it**, since a second stream would leave two claiming one /// namespace, and a withdrawal cancels the request (PUBLISH_NAMESPACE_DONE before /// draft-17). - async fn sync_namespace( - &self, - ns: &mut Namespaces, - suffix: &crate::PathOwned, - path: &crate::PathOwned, - ) -> Result<(), Error> { + async fn sync_namespace(&self, ns: &mut Namespaces, suffix: &crate::Path, path: &crate::Path) -> Result<(), Error> { let Some((advert, auth)) = self.offer(ns, suffix, path) else { return Ok(()); }; @@ -3769,7 +3766,7 @@ impl Publisher { return Ok(()); } - let absolute = self.origin.absolute(path).to_owned(); + let absolute = self.origin.root().rejoin(path); // Only a PUBLISH_NAMESPACE request or an update to one can be refused; everything // else below either rides a stream the peer already accepted or says nothing at all. let mut refused = watch.refused; @@ -3833,16 +3830,14 @@ impl Publisher { tracing::debug!(broadcast = %absolute, "namespace_done for an epoch gained or lost"); stream .writer - .encode_message(&ietf::NamespaceDone { - suffix: suffix.as_path(), - }) + .encode_message(&ietf::NamespaceDone { suffix: suffix.clone() }) .await?; } tracing::debug!(broadcast = %absolute, "namespace"); stream .writer .encode_message(&ietf::Namespace { - suffix: suffix.as_path(), + suffix: suffix.clone(), params: negotiated.extended().then(|| advert.params()), }) .await?; @@ -3851,9 +3846,7 @@ impl Publisher { tracing::debug!(broadcast = %absolute, "namespace_done"); stream .writer - .encode_message(&ietf::NamespaceDone { - suffix: suffix.as_path(), - }) + .encode_message(&ietf::NamespaceDone { suffix: suffix.clone() }) .await?; } // Never advertised and still not advertisable: nothing to say. @@ -3884,10 +3877,10 @@ impl Publisher { /// Returns what the refusal, if any, said about coming back. async fn advertise_namespace( &self, - requests: &mut HashMap, + requests: &mut HashMap, watch: &mut Watched, - path: &crate::PathOwned, - suffix: crate::PathOwned, + path: &crate::Path, + suffix: crate::Path, params: ietf::AdvertParams, auth: Option, ) -> Result { @@ -3897,7 +3890,7 @@ impl Publisher { // Giving up records nothing and spends no request ID, so the namespace is simply // retried later. let Some(mut request) = self.open_request().await? else { - tracing::debug!(broadcast = %self.origin.absolute(path), "no stream for the advertisement"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), path), "no stream for the advertisement"); return Ok(Refused::No); }; let request_id = self.control.next_request_id(&self.runtime).await?; @@ -3907,7 +3900,7 @@ impl Publisher { .writer .encode_message(&ietf::PublishNamespace { request_id, - track_namespace: path.as_path(), + track_namespace: path.clone(), params, authorization_token: auth.as_ref().and_then(|auth| self.request_token(auth)), }) @@ -3926,7 +3919,7 @@ impl Publisher { Err(err) => Err(err), }; - let absolute = self.origin.absolute(path); + let absolute = self.origin.root().rejoin(path); let super::request_stream::Framed(type_id, body) = match answer { Ok(Answer::Message(framed)) => framed, Ok(Answer::Late) => { @@ -4007,8 +4000,8 @@ impl Publisher { fn offer( &self, ns: &Namespaces, - suffix: &crate::PathOwned, - path: &crate::PathOwned, + suffix: &crate::Path, + path: &crate::Path, ) -> Option<(Advert, Option)> { let watch = ns.watched.get(suffix)?; let advert = match ns.permitted(path) { @@ -4038,13 +4031,13 @@ impl Publisher { /// Drop queued updates that no longer have anything to send, so the next one to take /// a request ID is one that will use it: drafts before 17 require the next ID. - fn prune_updates(&self, ns: &mut Namespaces, prefix: &crate::PathOwned) { + fn prune_updates(&self, ns: &mut Namespaces, prefix: &crate::Path) { while let Some(suffix) = ns.updates.front() { let wants = ns.requests.get(suffix).is_some_and(|request| { request.queued && request.pending.is_none() && ns.watched.get(suffix).is_some_and(|watch| { - self.offer(ns, suffix, &prefix.join(suffix)) + self.offer(ns, suffix, &prefix.rejoin(suffix)) .is_some_and(|(advert, auth)| { advert.wanted() && (advert != watch.sent || auth != request.auth) }) @@ -4068,14 +4061,9 @@ impl Publisher { /// peer set without reading it. A different original publisher is an update like any /// other: the receiver drains what it already serves from the old one and never splices /// the two. - async fn send_update( - &self, - ns: &mut Namespaces, - prefix: &crate::PathOwned, - request_id: RequestId, - ) -> Result<(), Error> { + async fn send_update(&self, ns: &mut Namespaces, prefix: &crate::Path, request_id: RequestId) -> Result<(), Error> { let suffix = ns.updates.pop_front().expect("allocated for a queued update"); - let Some((advert, auth)) = self.offer(ns, &suffix, &prefix.join(&suffix)) else { + let Some((advert, auth)) = self.offer(ns, &suffix, &prefix.rejoin(&suffix)) else { return Ok(()); }; let (Some(request), Some(watch)) = (ns.requests.get_mut(&suffix), ns.watched.get_mut(&suffix)) else { @@ -4095,7 +4083,7 @@ impl Publisher { if auth != request.auth { update.authorization_token = auth.as_ref().and_then(|auth| self.request_token(auth)); } - tracing::debug!(broadcast = %self.origin.absolute(&request.path), "publish_namespace update"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), &request.path), "publish_namespace update"); let written = request.stream.writer.encode_message(&update).await; if let Err(err) = written { // The peer ended the request before the update reached it, which ends only this @@ -4125,13 +4113,13 @@ impl Publisher { /// What came back for an update in flight, polling only the requests that woke. fn poll_answer( - requests: &mut HashMap, - inflight: &mut VecDeque<(crate::time::Instant, crate::PathOwned)>, + requests: &mut HashMap, + inflight: &mut VecDeque<(crate::time::Instant, crate::Path)>, woken: &Arc, now: crate::time::Instant, version: Version, cx: &mut std::task::Context<'_>, - ) -> Poll<(crate::PathOwned, Result)> { + ) -> Poll<(crate::Path, Result)> { // Registered before the take, so a wake landing between them is not lost. *woken.owner.lock().unwrap() = Some(cx.waker().clone()); let mut suffixes = std::mem::take(&mut *woken.suffixes.lock().unwrap()); @@ -4185,15 +4173,15 @@ impl Publisher { async fn answered( &self, ns: &mut Namespaces, - prefix: &crate::PathOwned, - suffix: crate::PathOwned, + prefix: &crate::Path, + suffix: crate::Path, answer: Result, ) -> Result<(), Error> { let Some(request) = ns.requests.get_mut(&suffix) else { return Ok(()); }; let idle = request.pending.take().is_none(); - let absolute = self.origin.absolute(&request.path).to_owned(); + let absolute = self.origin.root().rejoin(&request.path); // A reset stream ends only this request, like a FIN; a malformed answer is the peer's // violation and still ends the loop. @@ -4252,7 +4240,7 @@ impl Publisher { } tracing::debug!(message = ?msg, "publish_namespace update ok"); // Whatever changed while it was in flight goes out next. - return self.sync_namespace(ns, &suffix, &prefix.join(&suffix)).await; + return self.sync_namespace(ns, &suffix, &prefix.rejoin(&suffix)).await; } ietf::RequestError::ID => { let msg: ietf::RequestError = body.message(self.version)?; @@ -4379,8 +4367,8 @@ impl Publisher { async fn withdraw_namespace( &self, target: &mut Target, - requests: &mut HashMap, - suffix: crate::PathOwned, + requests: &mut HashMap, + suffix: crate::Path, ) -> Result<(), Error> { match target { Target::Requests(_) => { @@ -4392,9 +4380,7 @@ impl Publisher { Target::Inline(stream) => { stream .writer - .encode_message(&ietf::NamespaceDone { - suffix: suffix.as_path(), - }) + .encode_message(&ietf::NamespaceDone { suffix: suffix.clone() }) .await?; } } @@ -4402,12 +4388,7 @@ impl Publisher { } /// 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, - ) { + async fn end_request(&self, mut stream: Stream, path: &crate::Path, 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) { @@ -4419,7 +4400,7 @@ impl Publisher { let _ = stream .writer .encode_message(&ietf::PublishNamespaceDone { - track_namespace: path.as_path(), + track_namespace: path.clone(), request_id, }) .await; @@ -4431,8 +4412,8 @@ impl Publisher { /// 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) { - let suffixes: Vec = requests.keys().cloned().collect(); + async fn withdraw_requests(&self, target: &mut Target, requests: &mut HashMap) { + let suffixes: Vec = requests.keys().cloned().collect(); for suffix in suffixes { let _ = self.withdraw_namespace(target, requests, suffix).await; } @@ -4466,7 +4447,7 @@ impl Publisher { let mut ns = Namespaces::new(peer, Target::Requests(None)); ns.epochs = self.peer_setup.get().await.epoch; - self.run_namespaces(origin.announced(), crate::Path::empty().to_owned(), ns, Vec::new()) + self.run_namespaces(origin.announced(), crate::Path::empty(), ns, Vec::new()) .await } @@ -4482,11 +4463,11 @@ impl Publisher { async fn run_subscribe_namespace_stream( self, stream: Stream, - msg: ietf::SubscribeNamespace<'_>, + msg: ietf::SubscribeNamespace, ) -> Result<(), Error> { let gate = crate::auth::Gate::new( self.auth.clone(), - msg.namespace.to_owned(), + msg.namespace.clone(), crate::auth::Direction::Publish, ); self.serve_subscribe_namespace_stream(stream, msg, gate).await @@ -4495,15 +4476,15 @@ impl Publisher { async fn serve_subscribe_namespace_stream( self, mut stream: Stream, - msg: ietf::SubscribeNamespace<'_>, + msg: ietf::SubscribeNamespace, gate: crate::auth::Gate, ) -> Result<(), Error> { if let Some(error) = super::namespace::refusal(&msg.namespace, self.version) { return super::request_stream::reject(stream, error).await; } - let prefix = msg.namespace.to_owned(); + let prefix = msg.namespace.clone(); - tracing::debug!(prefix = %self.origin.absolute(&prefix), "subscribe_namespace stream"); + tracing::debug!(prefix = %crate::path::Rooted(self.origin.root(), &prefix), "subscribe_namespace stream"); // A prefix outside our scope (empty origin, or a token that doesn't grant it) // just means we have nothing to announce; respond with an empty set rather than @@ -4564,7 +4545,7 @@ impl Publisher { let initial = Self::snapshot(&mut announced); let count = initial .iter() - .filter(|update| permit.matches(prefix.join(&update.prefix).as_str())) + .filter(|update| permit.matches(prefix.rejoin(&update.prefix).as_str())) .filter(|update| self.select(&update.route, &peer, declared.epoch).wanted()) .count(); (initial, Some(count as u64)) @@ -4662,7 +4643,7 @@ impl Publisher { async fn run_namespaces( &self, mut announced: crate::announce::Consumer, - mut prefix: crate::PathOwned, + mut prefix: crate::Path, mut ns: Namespaces, initial: Vec, ) -> Result<(), Error> { @@ -4849,7 +4830,7 @@ impl Publisher { // A minimum wait the peer named is enforced by `sync_namespace`, which // every path goes through, so a namespace still inside one simply // makes no offer this turn. The next sweep is at most RETRY_MAX away. - let deferred: Vec = ns + let deferred: Vec = ns .watched .iter() .filter(|(_, watch)| watch.deferred) @@ -4857,15 +4838,15 @@ impl Publisher { .collect(); for suffix in deferred { - let path = prefix.join(&suffix); + let path = prefix.rejoin(&suffix); self.sync_namespace(&mut ns, &suffix, &path).await?; } } NamespaceEvent::Regrant(permit) => { ns.permit = permit; - let suffixes: Vec = ns.watched.keys().cloned().collect(); + let suffixes: Vec = ns.watched.keys().cloned().collect(); for suffix in suffixes { - let path = prefix.join(&suffix); + let path = prefix.rejoin(&suffix); self.sync_namespace(&mut ns, &suffix, &path).await?; } } @@ -4914,12 +4895,12 @@ impl Publisher { async fn apply_update( &self, ns: &mut Namespaces, - prefix: &crate::PathOwned, + prefix: &crate::Path, update: crate::announce::Announce, active: bool, ) -> Result<(), Error> { let suffix = update.prefix; - let path = prefix.join(&suffix); + let path = prefix.rejoin(&suffix); if active { // A repeat for a live suffix is a metadata update: keep the @@ -4935,7 +4916,7 @@ impl Publisher { // Only close out namespaces the peer actually saw. let held = ns.watched.remove(&suffix).is_some_and(|watch| watch.sent.wanted()); if held { - tracing::debug!(route = %self.origin.absolute(&path), "namespace_done"); + tracing::debug!(route = %crate::path::Rooted(self.origin.root(), &path), "namespace_done"); self.withdraw_namespace(&mut ns.target, &mut ns.requests, suffix) .await?; } @@ -6324,7 +6305,7 @@ fn buffer_object( /// One draft-14/15 advertisement: the PUBLISH_NAMESPACE request it rode on and /// what closes it out with PUBLISH_NAMESPACE_DONE. struct NamespaceRequest { - path: crate::PathOwned, + path: crate::Path, request_id: RequestId, stream: Stream, messages: super::request_stream::Messages, @@ -7448,7 +7429,7 @@ mod serve_tests { let stream = Stream::open(&mut crate::driver::Transport::new(session.clone()), version) .await .unwrap(); - let suffix: crate::PathOwned = "room".into(); + let suffix = crate::Path::from_static("room"); let mut requests = HashMap::new(); requests.insert( suffix.clone(), @@ -7741,12 +7722,12 @@ mod serve_tests { // Draft-22 dropped the Fetch Type tag, leaving only the filtered form. fetch_type: match version { Version::Draft22 => FetchType::Filtered { - namespace: crate::Path::new("missing"), + namespace: crate::Path::from_static("missing"), track: "video".into(), filter: Filter::Unfiltered, }, _ => FetchType::Standalone { - namespace: crate::Path::new("missing"), + namespace: crate::Path::from_static("missing"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 0, object: 1 }, @@ -9126,7 +9107,7 @@ mod serve_tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(0), - namespace: crate::Path::new("room"), + namespace: crate::Path::from_static("room"), hidden: false, }; let mut running = std::pin::pin!(h.publisher.clone().run_subscribe_namespace_stream(stream, msg)); @@ -9170,7 +9151,7 @@ mod serve_tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(0), - namespace: crate::Path::new("old"), + namespace: crate::Path::from_static("old"), hidden: false, }; let mut running = std::pin::pin!(h.publisher.clone().run_subscribe_namespace_stream(stream, msg)); @@ -9226,7 +9207,7 @@ mod serve_tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(0), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), hidden: false, }; let mut run = std::pin::pin!(h.publisher.clone().run_subscribe_namespace_stream(stream, msg)); @@ -9246,7 +9227,7 @@ mod serve_tests { max_age: None, new_group: None, request_id: RequestId(REQUEST_ID), - track_namespace: crate::Path::new("room"), + track_namespace: crate::Path::from_static("room"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -9320,7 +9301,7 @@ mod serve_tests { .unwrap(); let mut msg = subscribe(Filter::NextObject, None); - msg.track_namespace = crate::Path::new("absent"); + msg.track_namespace = crate::Path::from_static("absent"); h.publisher.clone().run_subscribe_stream(stream, msg).await.unwrap(); @@ -9412,7 +9393,7 @@ mod serve_tests { }; let status = ietf::TrackStatus { request_id: RequestId(REQUEST_ID), - track_namespace: crate::Path::new("room"), + track_namespace: crate::Path::from_static("room"), track_name: "video".into(), properties_wanted: true, epoch: Some(epoch.clone()), @@ -9498,7 +9479,7 @@ mod serve_tests { if version == Version::Draft17 { w.varint(0)?; // required_request_id_delta } - super::super::namespace::encode_namespace(w, &crate::Path::new("room"))?; + super::super::namespace::encode_namespace(w, &crate::Path::from_static("room"))?; w.string("video")?; encode_params!(w, version, 0x04 => 7u64, @@ -9561,7 +9542,7 @@ mod serve_tests { fn track_status(namespace: &str, wanted: bool, version: Version) -> Vec { let msg = ietf::TrackStatus { request_id: RequestId(REQUEST_ID), - track_namespace: crate::Path::new(namespace), + track_namespace: crate::Path::new(namespace).unwrap(), track_name: "video".into(), properties_wanted: wanted, epoch: None, @@ -9772,7 +9753,7 @@ mod serve_tests { stream, ietf::TrackStatus { request_id: RequestId(REQUEST_ID), - track_namespace: crate::Path::new("room"), + track_namespace: crate::Path::from_static("room"), track_name: "video".into(), properties_wanted: true, epoch: None, @@ -9936,13 +9917,13 @@ mod serve_tests { group_order: GroupOrder::Descending, fetch_type: if Filter::is_draft22(version) { FetchType::Filtered { - namespace: crate::Path::new("room"), + namespace: crate::Path::from_static("room"), track: "video".into(), filter: Filter::Unfiltered, } } else { FetchType::Standalone { - namespace: crate::Path::new("room"), + namespace: crate::Path::from_static("room"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 0, object: 0 }, @@ -9980,7 +9961,7 @@ mod serve_tests { let mut body = Vec::new(); let mut w = Encoder::new(&mut body, version.into()); RequestId(REQUEST_ID).encode(&mut w, version).unwrap(); - super::super::namespace::encode_namespace(&mut w, &crate::Path::new(namespace)).unwrap(); + super::super::namespace::encode_namespace(&mut w, &crate::Path::new(namespace).unwrap()).unwrap(); w.string(track).unwrap(); w.varint(u64::from(wait.is_some())).unwrap(); if let Some(wait) = wait { @@ -10030,6 +10011,56 @@ mod serve_tests { } } + /// A namespace that overflows `Path::MAX_BYTES` under a deep root is refused per + /// request, and the session stays open. + #[moq_clock::test] + async fn a_namespace_too_long_under_the_root_is_refused_per_request() { + for version in ALL_VERSIONS { + let h = serve(version); + let root = "r".repeat(crate::Path::MAX_BYTES - 2); + let scoped = h + ._origin + .consume() + .scope(root.as_str(), &crate::Patterns::from(crate::Pattern::all())) + .unwrap(); + let peer_setup = peer::PeerSetup::default(); + peer_setup.set(peer::Peer::default()); + let publisher = Publisher::new( + crate::time::sim(), + crate::driver::Transport::new(h.session.clone()), + scoped, + Control::new(None, false), + None, + peer_setup, + version, + ); + let stream = Stream::open(&mut crate::driver::Transport::new(h.session.clone()), version) + .await + .unwrap(); + let mark = h.log.writes.lock().unwrap().len(); + publisher + .run_subscribe_stream(stream, subscribe(Filter::NextObject, None)) + .await + .unwrap(); + + let wire = h.log.writes.lock().unwrap()[mark..].to_vec(); + let mut buf = Decoder::new(&wire, version.into()); + let code = match version { + Version::Draft14 => { + assert_eq!(buf.varint().unwrap(), ietf::SubscribeError::ID, "{version}"); + ietf::SubscribeError::decode(&mut buf, version).unwrap().error_code + } + _ => { + assert_eq!(buf.varint().unwrap(), ietf::RequestError::ID, "{version}"); + ietf::RequestError::decode(&mut buf, version).unwrap().error_code + } + }; + let expected = request::to_code(&Error::from(crate::path::TooLong), request::Kind::Subscribe, version); + assert_eq!(code, expected, "{version}"); + assert!(h.log.closes().is_empty(), "{version}"); + } + } + /// Legal requests we don't serve are refused NOT_SUPPORTED one at a time, and the /// session stays open for the next one. #[moq_clock::test] @@ -10111,7 +10142,7 @@ mod serve_tests { Some(subscribe_options) => { ietf::SubscribeNamespaceLegacy { request_id: RequestId(REQUEST_ID), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), subscribe_options, hidden: false, } @@ -10122,7 +10153,7 @@ mod serve_tests { None => { ietf::SubscribeNamespace { request_id: RequestId(REQUEST_ID), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), hidden: false, } .encode_msg(&mut w, version) @@ -11538,7 +11569,7 @@ mod serve_tests { /// A standalone FETCH of `room/`, spelled as [`standalone_fetch`] describes. fn fetch_range(version: Version, track: &'static str, start: Location, end: Location) -> FetchType<'static> { - let namespace = crate::Path::new("room"); + let namespace = crate::Path::from_static("room"); let track = track.into(); match Filter::is_draft22(version) { true => FetchType::Filtered { @@ -12313,7 +12344,7 @@ mod serve_tests { settle().await; let fetch_type = FetchType::Filtered { - namespace: crate::Path::new("room"), + namespace: crate::Path::from_static("room"), track: "video".into(), filter, }; @@ -12661,7 +12692,7 @@ mod serve_tests { subscriber_priority: 128, group_order: GroupOrder::Ascending, fetch_type: FetchType::Standalone { - namespace: crate::Path::new("room"), + namespace: crate::Path::from_static("room"), track: "video".into(), start: location, end: Location { group: 0, object: 1 }, @@ -12744,7 +12775,7 @@ mod serve_tests { subscriber_priority: 128, group_order: GroupOrder::Ascending, fetch_type: FetchType::Standalone { - namespace: crate::Path::new("room"), + namespace: crate::Path::from_static("room"), track: "video".into(), start: Location { group: 3, object: 0 }, end: Location { group: 4, object: 0 }, @@ -15828,7 +15859,7 @@ mod tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(1), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), hidden: false, }; let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg)); @@ -15900,7 +15931,7 @@ mod tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(1), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), hidden: false, }; let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg)); @@ -16179,7 +16210,7 @@ mod tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(1), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), hidden: false, }; let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg)); @@ -16481,7 +16512,7 @@ mod tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(1), - namespace: crate::Path::new(prefix), + namespace: crate::Path::new(prefix).unwrap(), hidden, }; let mut solicited = std::pin::pin!(publisher.clone().run_subscribe_namespace_stream(stream, msg)); @@ -16640,7 +16671,7 @@ mod tests { .unwrap(); let msg = ietf::SubscribeNamespace { request_id: RequestId(1), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), hidden: false, }; let mut run = std::pin::pin!(publisher.run_subscribe_namespace_stream(stream, msg)); @@ -16807,7 +16838,7 @@ mod tests { // The state a refused or failed offer leaves behind: the peer holds nothing, and // the loop is coming back to it on a timer. - let suffix: crate::PathOwned = crate::Path::new("from/peer").to_owned(); + let suffix: crate::Path = crate::Path::from_static("from/peer"); let mut watch = Watched::new(crate::origin::Route::default().with_hops(hops)); watch.deferred = true; @@ -16916,7 +16947,7 @@ mod tests { writer.varint(ietf::PublishNamespaceDone::ID).await.unwrap(); writer .encode(&ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("cam"), + track_namespace: crate::Path::from_static("cam"), request_id: RequestId(1), }) .await @@ -16934,7 +16965,7 @@ mod tests { writer.varint(ietf::PublishNamespaceCancel::ID).await.unwrap(); writer .encode(&ietf::PublishNamespaceCancel { - track_namespace: crate::Path::new("cam"), + track_namespace: crate::Path::from_static("cam"), request_id: RequestId(1), error_code: 0, reason_phrase: "gone".into(), @@ -18312,7 +18343,7 @@ mod tests { max_age: None, new_group: None, request_id: RequestId(1), - track_namespace: crate::Path::new("nothing/here"), + track_namespace: crate::Path::from_static("nothing/here"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -18390,7 +18421,7 @@ mod tests { ( "standalone", FetchType::Standalone { - namespace: crate::Path::new("nothing/here"), + namespace: crate::Path::from_static("nothing/here"), track: "video".into(), start: Location { group: 0, object: 0 }, end: Location { group: 1, object: 0 }, @@ -18399,7 +18430,7 @@ mod tests { ( "empty range", FetchType::Standalone { - namespace: crate::Path::new("nothing/here"), + namespace: crate::Path::from_static("nothing/here"), track: "video".into(), start: Location { group: 2, object: 0 }, end: Location { group: 1, object: 0 }, @@ -18769,7 +18800,7 @@ mod range_tests { max_age: None, new_group: None, request_id: RequestId(1), - track_namespace: crate::Path::new("broadcast"), + track_namespace: crate::Path::from_static("broadcast"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, diff --git a/rs/moq-net/src/ietf/publisher/tracks.rs b/rs/moq-net/src/ietf/publisher/tracks.rs index 145a0e21d83..4edcea281df 100644 --- a/rs/moq-net/src/ietf/publisher/tracks.rs +++ b/rs/moq-net/src/ietf/publisher/tracks.rs @@ -21,13 +21,13 @@ use crate::util::TaskSet; #[derive(Default)] pub(super) struct Listings { /// The prefix of every established SUBSCRIBE_TRACKS, by a key its guard removes. - prefixes: HashMap, + prefixes: HashMap, next: u64, /// On draft-18, the tracks a PUBLISH of ours subscribes the peer to, and how many /// SUBSCRIBEs of the peer's each track has: draft-18 allows the peer one subscription /// per track (section 5.1), so a PUBLISH joins neither kind. - published: HashMap<(crate::PathOwned, Arc), usize>, - subscribed: HashMap<(crate::PathOwned, Arc), usize>, + published: HashMap<(crate::Path, Arc), usize>, + subscribed: HashMap<(crate::Path, Arc), usize>, } /// Holds a SUBSCRIBE_TRACKS prefix as established until dropped. @@ -40,7 +40,7 @@ impl Established { /// Establish `prefix`, unless it overlaps an established request (section 10.19): two /// would match the same namespaces, and keeping them apart is also what lists each /// broadcast at most once per session. - fn new(listings: &kio::Shared, prefix: &crate::PathOwned) -> Option { + fn new(listings: &kio::Shared, prefix: &crate::Path) -> Option { let mut state = listings.lock(); if state .prefixes @@ -69,7 +69,7 @@ impl Drop for Established { /// until dropped. pub(super) struct Subscribed { listings: kio::Shared, - key: (crate::PathOwned, Arc), + key: (crate::Path, Arc), /// A SUBSCRIBE of the peer's, rather than a PUBLISH of ours. direct: bool, } @@ -77,17 +77,17 @@ pub(super) struct Subscribed { impl Subscribed { /// Claim `namespace`'s `track` for a PUBLISH of ours, unless the peer is subscribed to /// it already, by its SUBSCRIBE or another PUBLISH. - fn publish(listings: &kio::Shared, namespace: crate::PathOwned, track: Arc) -> Option { + fn publish(listings: &kio::Shared, namespace: crate::Path, track: Arc) -> Option { Self::claim(listings, (namespace, track), false) } /// Count the peer's SUBSCRIBE to `namespace`'s `track`, unless a PUBLISH of ours /// subscribes it already. pub(super) fn subscribe(listings: &kio::Shared, namespace: &crate::Path, track: &str) -> Option { - Self::claim(listings, (namespace.to_owned(), Arc::from(track)), true) + Self::claim(listings, (namespace.clone(), Arc::from(track)), true) } - fn claim(listings: &kio::Shared, key: (crate::PathOwned, Arc), direct: bool) -> Option { + fn claim(listings: &kio::Shared, key: (crate::Path, Arc), direct: bool) -> Option { let mut locked = listings.lock(); let Listings { published, subscribed, .. @@ -142,14 +142,14 @@ struct Params { /// (suffix, name) in listing order. A track removed before its skip went out cancels it. /// A FORWARD=1 listing adds one only once this is empty, so a peer that stops reading /// leaves the rest unread in each broadcast's listing. -type Skips = kio::Shared), ()>>; +type Skips = kio::Shared), ()>>; /// What a request's listing shares with the tasks listing each broadcast. #[derive(Clone)] struct Request { origin: origin::Consumer, /// The request's prefix, which every PUBLISH namespace starts with. - prefix: crate::PathOwned, + prefix: crate::Path, /// Whether the request asked for every track's objects, which we answer with skips. forward: bool, params: Params, @@ -184,14 +184,14 @@ struct Entries { } impl Entries { - fn skip(&mut self, skips: &Skips, suffix: &crate::PathOwned, name: Arc) { + fn skip(&mut self, skips: &Skips, suffix: &crate::Path, name: Arc) { skips.lock().insert((suffix.clone(), name.clone()), ()); self.skipped.insert(name); } /// `name` is no longer listed: drop whatever it got to, ending its PUBLISH with /// TRACK_ENDED if it `finished`. - fn remove(&mut self, skips: &Skips, suffix: &crate::PathOwned, name: Arc, finished: bool) { + fn remove(&mut self, skips: &Skips, suffix: &crate::Path, name: Arc, finished: bool) { if self.staged.as_ref().is_some_and(|staged| staged.name == name) { self.staged = None; } @@ -208,7 +208,7 @@ impl Entries { } /// Nothing more is PUBLISHed or skipped, while the PUBLISHes already sent stay listed. - fn halt(&mut self, skips: &Skips, suffix: &crate::PathOwned) { + fn halt(&mut self, skips: &Skips, suffix: &crate::Path) { self.pending.clear(); self.staged = None; let mut skips = skips.lock(); @@ -218,7 +218,7 @@ impl Entries { } /// Every track is no longer listed. - fn unlist(&mut self, skips: &Skips, suffix: &crate::PathOwned) { + fn unlist(&mut self, skips: &Skips, suffix: &crate::Path) { self.halt(skips, suffix); self.listed.clear(); } @@ -239,7 +239,7 @@ struct Listed { stream: Stream, track: track::Consumer, /// The track's full namespace, as the PUBLISH names it. - namespace: crate::PathOwned, + namespace: crate::Path, name: Arc, properties: ietf::Properties, /// Closes once the track is no longer listed, holding whether it finished. @@ -283,7 +283,7 @@ impl Publisher { pub(super) async fn serve_subscribe_tracks_stream( self, mut stream: Stream, - msg: ietf::SubscribeTracks<'_>, + msg: ietf::SubscribeTracks, ) -> Result<(), Error> { if let Some(error) = super::super::namespace::refusal(&msg.prefix, self.version) { return super::super::request_stream::reject(stream, error).await; @@ -310,13 +310,13 @@ impl Publisher { let error = refuse(request::NOT_SUPPORTED, "SUBSCRIBE_TRACKS needs FORWARD=0"); return super::super::request_stream::reject(stream, error).await; } - let prefix = msg.prefix.to_owned(); + let prefix = msg.prefix.clone(); let Some(established) = Established::new(&self.listings, &prefix) else { let error = refuse(request::PREFIX_OVERLAP, "overlaps another SUBSCRIBE_TRACKS"); return super::super::request_stream::reject(stream, error).await; }; - tracing::debug!(prefix = %self.origin.absolute(&prefix), forward = msg.forward, "subscribe_tracks stream"); + tracing::debug!(prefix = %crate::path::Rooted(self.origin.root(), &prefix), forward = msg.forward, "subscribe_tracks stream"); // The view SUBSCRIBE_NAMESPACE discovers through: rooted at the prefix so every // broadcast arrives named by its suffix, never handing back what flowed through the @@ -365,8 +365,8 @@ impl Publisher { live: live.consume(), }; // Each listed broadcast's route, closed once its announcement goes; see [`Route`]. - let mut routes: HashMap> = HashMap::new(); - let withdraw = |routes: &mut HashMap>, prefix: &crate::PathOwned| { + let mut routes: HashMap> = HashMap::new(); + let withdraw = |routes: &mut HashMap>, prefix: &crate::Path| { if let Some(route) = routes.remove(prefix) && let Ok(mut route) = route.write() { @@ -436,7 +436,7 @@ impl Publisher { broadcasts.push(task); }; - tracing::debug!(prefix = %self.origin.absolute(&prefix), ?cancelled, "subscribe_tracks ended"); + tracing::debug!(prefix = %crate::path::Rooted(self.origin.root(), &prefix), ?cancelled, "subscribe_tracks ended"); // The request no longer stands: another may take its prefix, nothing more is // PUBLISHed, and what was never sent is dropped. Its routes stay, so a PUBLISH // already sent still ends when its track does. @@ -598,12 +598,12 @@ impl Publisher { async fn run_listed_broadcast( self, request: Request, - suffix: crate::PathOwned, + suffix: crate::Path, epoch: Option, route: kio::Consumer, ) { - let namespace = request.prefix.join(&suffix).to_owned(); - let absolute = self.origin.absolute(&namespace).to_owned(); + let namespace = request.prefix.rejoin(&suffix); + let absolute = self.origin.root().rejoin(&namespace); let flate = self.peer_setup.get().await.flate; // Resolving may wait on a route for the broadcast, which its going ends. @@ -868,7 +868,7 @@ impl Publisher { // An update may ask for a new group only if the PUBLISH sent DYNAMIC_GROUPS, which // INCLUDE_PROPERTIES=0 leaves out. let dynamic = properties.dynamic_groups; - let absolute = self.origin.absolute(&namespace).to_owned(); + let absolute = self.origin.root().rejoin(&namespace); // 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 @@ -1167,7 +1167,7 @@ impl Publisher { &self, stream: &mut Stream, track: &track::Consumer, - namespace: &crate::PathOwned, + namespace: &crate::Path, request_id: RequestId, params: &mut Params, how: Resume, @@ -1407,10 +1407,10 @@ mod tests { } } - fn listing(forward: bool) -> ietf::SubscribeTracks<'static> { + fn listing(forward: bool) -> ietf::SubscribeTracks { ietf::SubscribeTracks { request_id: RequestId(0), - prefix: crate::Path::new("room"), + prefix: crate::Path::from_static("room"), forward, subscriber_priority: None, group_order: None, @@ -1796,7 +1796,7 @@ mod tests { let mut nested = listing(false); nested.request_id = RequestId(2); - nested.prefix = crate::Path::new("room/cam"); + nested.prefix = crate::Path::from_static("room/cam"); let mut second = h.request(&nested).await; let (id, body) = next(&mut second).await; assert_eq!(id, ietf::RequestError::ID, "{version}"); @@ -1822,7 +1822,7 @@ mod tests { let subscribe = ietf::Subscribe { request_id: RequestId(2), - track_namespace: crate::Path::new("room/cam"), + track_namespace: crate::Path::from_static("room/cam"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -1911,7 +1911,7 @@ mod tests { let route = kio::Producer::new(Route::Replaced); let request = Request { origin: h._origin.consume(), - prefix: crate::Path::new("").to_owned(), + prefix: crate::Path::from_static(""), forward: true, params: Params { priority: crate::ietf::priority::DEFAULT_WIRE, @@ -1926,7 +1926,7 @@ mod tests { }; moq_clock::sim::spawn(h.publisher.clone().run_listed_broadcast( request, - crate::Path::new("room/cam").to_owned(), + crate::Path::from_static("room/cam"), None, route.consume(), )); @@ -2337,7 +2337,7 @@ mod tests { let mut h = harness(version); let subscribe = ietf::Subscribe { request_id: RequestId(0), - track_namespace: crate::Path::new("room/cam"), + track_namespace: crate::Path::from_static("room/cam"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, diff --git a/rs/moq-net/src/ietf/request_stream.rs b/rs/moq-net/src/ietf/request_stream.rs index 49d4528292b..bd942c282f6 100644 --- a/rs/moq-net/src/ietf/request_stream.rs +++ b/rs/moq-net/src/ietf/request_stream.rs @@ -515,12 +515,12 @@ impl Decode for Update { #[derive(Debug)] pub(super) struct NamespaceUpdate { pub(super) request_id: super::RequestId, - pub(super) prefix: Option, + pub(super) prefix: Option, pub(super) refused: Option, } #[derive(Debug)] -struct NamespacePrefix(crate::PathOwned); +struct NamespacePrefix(crate::Path); impl super::parameters::Param for NamespacePrefix { fn param_encode(&self, w: &mut crate::coding::Encoder<'_>, _: Version) -> Result<(), crate::coding::EncodeError> { @@ -594,7 +594,7 @@ mod tests { use crate::lite::test_transport::ScriptedSession; use crate::stats::{Message, Registry, Tier}; let registry = Registry::new(Default::default()); - let stats = registry.tier(Tier::default()).session(""); + let stats = registry.tier(Tier::default()).session(crate::Path::from_static("")); let version = Version::Draft22; let session = ScriptedSession::new(vec![0x10, 0, 2, 0, 0, 0x07, 0, 0]); let mut stream = with_stats( diff --git a/rs/moq-net/src/ietf/session.rs b/rs/moq-net/src/ietf/session.rs index 44255e53691..0720af9f3fb 100644 --- a/rs/moq-net/src/ietf/session.rs +++ b/rs/moq-net/src/ietf/session.rs @@ -1548,7 +1548,7 @@ async fn enforce_grant(auth: crate::auth::Link, publisher: Publisher, mut sessio let Some(path) = kio::wait(|waiter| check.poll(&auth, &mut announced, waiter)).await else { return Ok(()); }; - tracing::error!(broadcast = %origin.absolute(&path), "publishing outside our grant; closing the session"); + tracing::error!(broadcast = %crate::path::Rooted(origin.root(), &path), "publishing outside our grant; closing the session"); let err = Error::Unauthorized; session.close( super::error::to_session_code(&SessionError::from(&err)), @@ -1679,7 +1679,7 @@ mod tests { } else { ietf::TrackStatus { request_id: RequestId(id), - track_namespace: crate::Path::from("test"), + track_namespace: crate::Path::from_static("test"), track_name: "video".into(), properties_wanted: true, epoch: None, @@ -1995,7 +1995,7 @@ mod tests { writer.varint(ietf::Namespace::ID).await.unwrap(); writer .encode(&ietf::Namespace { - suffix: crate::Path::new("cam"), + suffix: crate::Path::from_static("cam"), params: None, }) .await @@ -2142,7 +2142,7 @@ mod tests { ); let msg = ietf::SubscribeNamespaceLegacy { request_id, - namespace: crate::Path::new(namespace), + namespace: crate::Path::new(namespace).unwrap(), subscribe_options: ietf::SubscribeOptions::Namespace, hidden: false, }; @@ -2156,7 +2156,7 @@ mod tests { 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), + prefix: crate::Path::new(prefix).unwrap(), request_id, }; writer.varint(ietf::UnsubscribeNamespace::ID).await.unwrap(); @@ -3127,7 +3127,7 @@ mod tests { max_age: None, new_group: None, request_id: RequestId(1), - track_namespace: crate::Path::new("room"), + track_namespace: crate::Path::from_static("room"), track_name: "video".into(), subscriber_priority: 128, group_order: ietf::GroupOrder::Descending, @@ -3448,7 +3448,7 @@ mod tests { writer .encode(&ietf::PublishNamespace { request_id: RequestId(1), - track_namespace: crate::Path::new("room/host"), + track_namespace: crate::Path::from_static("room/host"), params: Default::default(), authorization_token: None, }) @@ -3459,7 +3459,7 @@ mod tests { writer.varint(ietf::PublishNamespaceDone::ID).await.unwrap(); writer .encode(&ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("room/host"), + track_namespace: crate::Path::from_static("room/host"), request_id: RequestId(0), }) .await @@ -3558,7 +3558,7 @@ mod tests { max_age: None, new_group: None, request_id: RequestId(0), - track_namespace: crate::Path::new("room"), + track_namespace: crate::Path::from_static("room"), track_name: "video".into(), subscriber_priority: 128, group_order: ietf::GroupOrder::Descending, @@ -4115,7 +4115,7 @@ mod tests { let mut body = Vec::new(); ietf::SubscribeNamespace { request_id: RequestId(request_id), - namespace: crate::Path::new(""), + namespace: crate::Path::from_static(""), hidden: false, } .encode_msg(&mut crate::coding::Encoder::new(&mut body, version.into()), version) diff --git a/rs/moq-net/src/ietf/subscribe.rs b/rs/moq-net/src/ietf/subscribe.rs index cdaf65a968c..182939b3b08 100644 --- a/rs/moq-net/src/ietf/subscribe.rs +++ b/rs/moq-net/src/ietf/subscribe.rs @@ -21,7 +21,7 @@ use super::Version; #[derive(Clone, Debug)] pub struct Subscribe<'a> { pub request_id: RequestId, - pub track_namespace: Path<'a>, + pub track_namespace: Path, pub track_name: Cow<'a, str>, pub subscriber_priority: u8, pub group_order: GroupOrder, @@ -624,7 +624,7 @@ mod tests { let msg = Subscribe { max_age, request_id: RequestId(0), - track_namespace: Path::new("live"), + track_namespace: Path::from_static("live"), track_name: "catalog".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -666,7 +666,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: Path::new("test"), + track_namespace: Path::from_static("test"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -695,7 +695,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: Path::new("test"), + track_namespace: Path::from_static("test"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -738,7 +738,7 @@ mod tests { new_group: Some(7), max_age: None, request_id: RequestId(1), - track_namespace: Path::new("test"), + track_namespace: Path::from_static("test"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -781,7 +781,7 @@ mod tests { if version == Version::Draft17 { w.varint(0)?; // required_request_id_delta } - encode_namespace(w, &Path::new("test"))?; + encode_namespace(w, &Path::from_static("test"))?; w.string("video")?; encode_params!(w, version, 0x04 => millis, @@ -848,7 +848,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: Path::new("test"), + track_namespace: Path::from_static("test"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -884,7 +884,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(100), - track_namespace: Path::new("conference/room123"), + track_namespace: Path::from_static("conference/room123"), track_name: "audio".into(), subscriber_priority: 255, group_order: GroupOrder::Descending, @@ -1046,7 +1046,7 @@ mod tests { let mut body = Vec::new(); let w = &mut Encoder::new(&mut body, version.into()); RequestId(1).encode(w, version)?; - encode_namespace(w, &crate::Path::new("broadcast"))?; + encode_namespace(w, &crate::Path::from_static("broadcast"))?; w.string("video")?; encode_params!(w, version, 0x02 => 5000u64, @@ -1074,7 +1074,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: crate::Path::new("broadcast"), + track_namespace: crate::Path::from_static("broadcast"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -1253,7 +1253,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: Path::new("test"), + track_namespace: Path::from_static("test"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -1323,7 +1323,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: Path::new("test"), + track_namespace: Path::from_static("test"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -1649,7 +1649,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: crate::Path::new("live"), + track_namespace: crate::Path::from_static("live"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, @@ -1713,7 +1713,7 @@ mod tests { new_group: None, max_age: None, request_id: RequestId(1), - track_namespace: crate::Path::new("live"), + track_namespace: crate::Path::from_static("live"), track_name: "video".into(), subscriber_priority: 128, group_order: GroupOrder::Descending, diff --git a/rs/moq-net/src/ietf/subscribe_namespace.rs b/rs/moq-net/src/ietf/subscribe_namespace.rs index 15ccfc51f96..2ee683a69c1 100644 --- a/rs/moq-net/src/ietf/subscribe_namespace.rs +++ b/rs/moq-net/src/ietf/subscribe_namespace.rs @@ -26,9 +26,9 @@ fn is_legacy_version(version: Version) -> bool { /// Options field when it split SUBSCRIBE_TRACKS (0x51) off into its own message /// type (#1542). Draft-14 through draft-17 use [`SubscribeNamespaceLegacy`]. #[derive(Clone, Debug)] -pub struct SubscribeNamespace<'a> { +pub struct SubscribeNamespace { pub request_id: RequestId, - pub namespace: Path<'a>, + pub namespace: Path, /// MoQ Hidden: also advertise hidden namespaces. Only sent to a peer that declared /// the extension; absent means false. pub hidden: bool, @@ -48,7 +48,7 @@ fn hidden_from_param(value: Option) -> Result { } } -impl Message for SubscribeNamespace<'_> { +impl Message for SubscribeNamespace { const ID: u64 = 0x50; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -124,16 +124,16 @@ impl SubscribeOptions { /// Required Request ID delta (removed in draft-18 per #1615). Draft-18+ uses /// [`SubscribeNamespace`]. #[derive(Clone, Debug)] -pub struct SubscribeNamespaceLegacy<'a> { +pub struct SubscribeNamespaceLegacy { pub request_id: RequestId, - pub namespace: Path<'a>, + pub namespace: Path, /// 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<'_> { +impl Message for SubscribeNamespaceLegacy { const ID: u64 = 0x11; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -243,14 +243,14 @@ impl Message for SubscribeNamespaceError<'_> { /// 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> { +pub struct UnsubscribeNamespace { /// v14: the Track Namespace Prefix of the SUBSCRIBE_NAMESPACE it cancels. - pub prefix: Path<'a>, + pub prefix: Path, /// v15: the Request ID of the SUBSCRIBE_NAMESPACE it cancels. pub request_id: RequestId, } -impl Message for UnsubscribeNamespace<'_> { +impl Message for UnsubscribeNamespace { const ID: u64 = 0x14; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -285,15 +285,15 @@ impl Message for UnsubscribeNamespace<'_> { /// field for both, so the advertisement can carry its HOP_PATH and ROUTE_COST (see /// [`super::cluster`]) and its EPOCH (see [`super::epoch`]). #[derive(Clone, Debug)] -pub struct Namespace<'a> { - pub suffix: Path<'a>, +pub struct Namespace { + pub suffix: Path, /// The extension parameters. `Some` selects the extended form; `None` the base one. /// An endpoint must not append them on a session that negotiated neither extension. pub params: Option, } -impl Namespace<'_> { +impl Namespace { /// Decode the message body, expecting the extended form when the session negotiated /// an extension that defines it. See [`super::PublishNamespace::decode_body`]. pub fn decode_body(r: &mut Decoder<'_>, version: Version, negotiated: Negotiated) -> Result { @@ -310,7 +310,7 @@ impl Namespace<'_> { } } -impl Message for Namespace<'_> { +impl Message for Namespace { const ID: u64 = 0x08; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -329,11 +329,11 @@ impl Message for Namespace<'_> { /// NAMESPACE_DONE message (0x0E): v16 only, sent on SUBSCRIBE_NAMESPACE bidi stream /// Indicates a namespace suffix matching the subscribed prefix is no longer active. #[derive(Clone, Debug)] -pub struct NamespaceDone<'a> { - pub suffix: Path<'a>, +pub struct NamespaceDone { + pub suffix: Path, } -impl Message for NamespaceDone<'_> { +impl Message for NamespaceDone { const ID: u64 = 0x0E; fn encode_msg(&self, w: &mut Encoder<'_>, _: Version) -> Result<(), EncodeError> { @@ -377,11 +377,7 @@ mod tests { epoch: true, }; - fn decode( - bytes: Vec, - version: Version, - negotiated: Negotiated, - ) -> Result<(Namespace<'static>, bool), DecodeError> { + fn decode(bytes: Vec, version: Version, negotiated: Negotiated) -> Result<(Namespace, bool), DecodeError> { let mut buf = bytes::Bytes::from(bytes); let decoded = crate::coding::decode_buf(&mut buf, version, |r, v| Namespace::decode_body(r, v, negotiated))?; Ok((decoded, buf.is_empty())) @@ -392,7 +388,7 @@ mod tests { #[test] fn unsubscribe_namespace_names_the_prefix_then_the_request() { let msg = UnsubscribeNamespace { - prefix: Path::new("cam/front"), + prefix: Path::from_static("cam/front"), request_id: RequestId(6), }; let mut v14 = bytes::Bytes::from(body(&msg, Version::Draft14)); @@ -426,7 +422,7 @@ mod tests { fn cluster_namespace_round_trips() { let version = Version::Draft18; let msg = Namespace { - suffix: Path::new("alice.hang"), + suffix: Path::from_static("alice.hang"), params: clustered(&[7, 9], 5), }; @@ -439,7 +435,7 @@ mod tests { // The base form is strictly shorter, and reading the extended bytes as base // leaves the parameters behind rather than silently absorbing them. let base = Namespace { - suffix: Path::new("alice.hang"), + suffix: Path::from_static("alice.hang"), params: None, }; let base_encoded = body(&base, version); @@ -455,11 +451,11 @@ mod tests { fn cluster_namespace_omits_zero_cost() { let version = Version::Draft18; let free = Namespace { - suffix: Path::new("a"), + suffix: Path::from_static("a"), params: clustered(&[7], 0), }; let priced = Namespace { - suffix: Path::new("a"), + suffix: Path::from_static("a"), params: clustered(&[7], 1), }; assert!(body(&free, version).len() < body(&priced, version).len()); @@ -476,7 +472,7 @@ mod tests { let epoch = crate::Epoch::mint(); for epoch in [None, Some(epoch)] { let msg = Namespace { - suffix: Path::new("a"), + suffix: Path::from_static("a"), params: Some(AdvertParams { cluster: None, epoch: epoch.clone(), @@ -495,7 +491,7 @@ mod tests { let mut params = clustered(&[7], 3).unwrap(); params.epoch = Some(crate::Epoch::mint()); let msg = Namespace { - suffix: Path::new("a"), + suffix: Path::from_static("a"), params: Some(params), }; let encoded = body(&msg, version); @@ -505,7 +501,7 @@ mod tests { // Read as the base form, a single block is all that is left over. let mut suffix = Vec::new(); - encode_namespace(&mut Encoder::new(&mut suffix, version.into()), &Path::new("a")).unwrap(); + encode_namespace(&mut Encoder::new(&mut suffix, version.into()), &Path::from_static("a")).unwrap(); let mut block = Vec::new(); super::super::publish_namespace::encode_advert_params( &mut Encoder::new(&mut block, version.into()), @@ -522,7 +518,7 @@ mod tests { fn epoch_refused_unless_negotiated_and_well_formed() { let version = Version::Draft22; let msg = Namespace { - suffix: Path::new("a"), + suffix: Path::from_static("a"), params: Some(AdvertParams { cluster: clustered(&[7], 0).unwrap().cluster, epoch: Some(crate::Epoch::mint()), @@ -534,7 +530,7 @@ mod tests { )); let mut buf = Vec::new(); - encode_namespace(&mut Encoder::new(&mut buf, version.into()), &Path::new("a")).unwrap(); + encode_namespace(&mut Encoder::new(&mut buf, version.into()), &Path::from_static("a")).unwrap(); let w = &mut Encoder::new(&mut buf, version.into()); w.varint(1).unwrap(); w.varint(super::super::epoch::EPOCH_PARAM).unwrap(); @@ -547,7 +543,7 @@ mod tests { fn cluster_namespace_requires_hop_path() { let version = Version::Draft18; let mut buf = Vec::new(); - encode_namespace(&mut Encoder::new(&mut buf, version.into()), &Path::new("a")).unwrap(); + encode_namespace(&mut Encoder::new(&mut buf, version.into()), &Path::from_static("a")).unwrap(); // Number of Parameters = 0. Encoder::new(&mut buf, version.into()).varint(0).unwrap(); @@ -565,7 +561,7 @@ mod tests { let version = Version::Draft18; let msg = super::super::PublishNamespace { request_id: RequestId(1), - track_namespace: Path::new("a"), + track_namespace: Path::from_static("a"), params: AdvertParams { cluster: Some(cluster::Advert { hops: hop_path(&[7]), @@ -666,7 +662,7 @@ mod tests { fn modern_round_trips() { let msg = SubscribeNamespace { request_id: RequestId(4), - namespace: Path::new("example/meeting"), + namespace: Path::from_static("example/meeting"), hidden: false, }; let mut buf = bytes::Bytes::from(body(&msg, Version::Draft18)); @@ -682,7 +678,7 @@ mod tests { for hidden in [false, true] { let msg = SubscribeNamespace { request_id: RequestId(4), - namespace: Path::new("example"), + namespace: Path::from_static("example"), hidden, }; let mut buf = bytes::Bytes::from(body(&msg, Version::Draft18)); @@ -696,7 +692,7 @@ mod tests { for version in [Version::Draft14, Version::Draft16, Version::Draft17] { let msg = SubscribeNamespaceLegacy { request_id: RequestId(4), - namespace: Path::new("example"), + namespace: Path::from_static("example"), subscribe_options: SubscribeOptions::Namespace, hidden, }; @@ -718,7 +714,7 @@ mod tests { ] { let msg = SubscribeNamespaceLegacy { request_id: RequestId(4), - namespace: Path::new("example/meeting"), + namespace: Path::from_static("example/meeting"), subscribe_options: options, hidden: false, }; diff --git a/rs/moq-net/src/ietf/subscribe_tracks.rs b/rs/moq-net/src/ietf/subscribe_tracks.rs index 96cfe4bed77..26a05e949f9 100644 --- a/rs/moq-net/src/ietf/subscribe_tracks.rs +++ b/rs/moq-net/src/ietf/subscribe_tracks.rs @@ -19,9 +19,9 @@ use super::token::RequestToken; /// /// Its subscription parameters are the defaults of each PUBLISH it produces. #[derive(Clone, Debug)] -pub struct SubscribeTracks<'a> { +pub struct SubscribeTracks { pub request_id: RequestId, - pub prefix: Path<'a>, + pub prefix: Path, /// FORWARD: absent means 1, which asks each PUBLISH to push its objects at once. pub forward: bool, /// The default subscriber priority of each PUBLISH subscription. @@ -44,7 +44,7 @@ pub struct SubscribeTracks<'a> { pub authorization_token: Option, } -impl Message for SubscribeTracks<'_> { +impl Message for SubscribeTracks { const ID: u64 = 0x51; fn encode_msg(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { @@ -142,7 +142,7 @@ pub(crate) fn has_subscribe_tracks(version: Version) -> bool { #[derive(Clone, Debug)] pub struct PublishSkipped<'a> { /// The track's namespace below the request's prefix. - pub suffix: Path<'a>, + pub suffix: Path, pub track_name: Cow<'a, str>, } @@ -190,10 +190,10 @@ mod tests { crate::coding::decode_buf(&mut buf, version, M::decode_msg) } - fn paused() -> SubscribeTracks<'static> { + fn paused() -> SubscribeTracks { SubscribeTracks { request_id: RequestId(4), - prefix: Path::new("room"), + prefix: Path::from_static("room"), forward: false, subscriber_priority: None, group_order: None, @@ -297,7 +297,7 @@ mod tests { fn publish_skipped_round_trips() { for version in [Version::Draft17, Version::Draft18, Version::Draft22] { let msg = PublishSkipped { - suffix: Path::new("cam"), + suffix: Path::from_static("cam"), track_name: "video".into(), }; let decoded: PublishSkipped = decode(&encode(&msg, version), version).unwrap(); diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index 54de30565d6..78f0496c0a6 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -8,7 +8,7 @@ use std::{ }; use crate::{ - Error, Path, PathOwned, SessionError, Timescale, broadcast, + Error, Path, SessionError, Timescale, broadcast, coding::{Decode, DecodeError, Decoder, Reader, Stream, Writer}, frame, group, ietf::{self, Control, FetchType, Filter, GroupOrder, RequestId}, @@ -202,7 +202,7 @@ struct State { aliases: TrackAliases, // Each broadcast created by a PUBLISH_NAMESPACE message. - broadcasts: HashMap, + broadcasts: HashMap, // Names each advertisement the peer makes, so one that a newer advertisement of the // same namespace replaced cannot overwrite it. @@ -754,7 +754,7 @@ struct TrackState { // it names, it forms the full track name, which is what decides whether a repeated // alias is the fatal collision (one alias, two tracks) or the legal sharing of an alias // across subscriptions. - broadcast: PathOwned, + broadcast: Path, epoch: Option, // Units for this track's object Timestamps, from the TIMESCALE Track Property in @@ -790,13 +790,13 @@ impl TrackState { } #[cfg(test)] - fn new(producer: track::Producer, broadcast: PathOwned, fill: kio::Producer) -> Self { + fn new(producer: track::Producer, broadcast: Path, fill: kio::Producer) -> Self { let mut state = Self::pending(producer.name().to_owned(), broadcast, fill); state.producer = Some(producer); state } - fn pending(name: String, broadcast: PathOwned, fill: kio::Producer) -> Self { + fn pending(name: String, broadcast: Path, fill: kio::Producer) -> Self { Self { producer: None, pending: None, @@ -846,8 +846,8 @@ struct BroadcastState { } /// A namespace request and the authorization selected at dispatch. -struct IncomingNamespace<'a> { - msg: ietf::PublishNamespace<'a>, +struct IncomingNamespace { + msg: ietf::PublishNamespace, peer: cluster::Peer, declared: Option, gate: crate::auth::Gate, @@ -1013,13 +1013,13 @@ pub(super) struct Subscriber { struct NamespaceRequest { stream: Stream, messages: super::request_stream::Messages, - prefix: PathOwned, + prefix: Path, /// Set once the request goes out. request_id: Option, /// The suffixes this stream advertised, with the advertisement each one holds, so a /// repeat is recognized as an update to it rather than a second one (which would /// leak the refcount). - live: HashMap, + live: HashMap, } /// The initial set of a SUBSCRIBE_NAMESPACE still to read, under MoQ Active Count. @@ -1150,7 +1150,7 @@ impl Subscriber { } /// Serve the requests beneath one attached namespace on its own task. - fn serve_route(&self, path: PathOwned, generation: u64) { + fn serve_route(&self, path: Path, generation: u64) { let this = self.clone(); self.tasks.push(async move { // stop_announce is the authoritative remover: it drops the entry @@ -1372,7 +1372,7 @@ impl Subscriber { async fn run_subscribe_namespace( &mut self, stream: Stream, - prefix: PathOwned, + prefix: Path, snapshot: crate::model::Snapshot, live: kio::Consumer<()>, ) -> Result { @@ -1641,7 +1641,7 @@ impl Subscriber { return Err(Error::WrongSize); } initial.initial = initial.initial.saturating_sub(1); - let path = prefix.join(&msg.suffix); + let path = prefix.join(&msg.suffix)?; let params = msg.params.unwrap_or_default(); let held = match live.entry(path.clone()) { // A repeat under another epoch replaces the instance in place, but one that @@ -1701,7 +1701,7 @@ impl Subscriber { ietf::NamespaceDone::ID if initial.initial > 0 => return Err(Error::ProtocolViolation), ietf::NamespaceDone::ID => { let msg = ietf::NamespaceDone::decode_msg(&mut data, self.version)?; - let path = prefix.join(&msg.suffix); + let path = prefix.join(&msg.suffix)?; tracing::debug!(%path, "namespace_done"); if let Some(id) = live.remove(&path).and_then(|held| held.id) { let _ = self.stop_announce(path, id, false); @@ -1778,7 +1778,7 @@ impl Subscriber { tracing::debug!(message = ?msg, "received publish_namespace"); let gate = crate::auth::Gate::new( this.auth.clone(), - msg.track_namespace.to_owned(), + msg.track_namespace.clone(), crate::auth::Direction::Subscribe, ); let incoming = IncomingNamespace { @@ -1866,13 +1866,13 @@ impl Subscriber { async fn run_publish_namespace_stream( &mut self, stream: Stream, - msg: ietf::PublishNamespace<'_>, + msg: ietf::PublishNamespace, peer: cluster::Peer, declared: Option, ) -> Result<(), Error> { let gate = crate::auth::Gate::new( self.auth.clone(), - msg.track_namespace.to_owned(), + msg.track_namespace.clone(), crate::auth::Direction::Subscribe, ); self.serve_publish_namespace_stream( @@ -1890,7 +1890,7 @@ impl Subscriber { async fn serve_publish_namespace_stream( &mut self, mut stream: Stream, - incoming: IncomingNamespace<'_>, + incoming: IncomingNamespace, ) -> Result<(), Error> { let IncomingNamespace { msg, @@ -1902,7 +1902,7 @@ impl Subscriber { return super::request_stream::reject(stream, error).await; } let request_id = msg.request_id; - let path = msg.track_namespace.to_owned(); + let path = msg.track_namespace.clone(); if self.unsolicited_is_a_violation(declared) { tracing::warn!(%path, "unsolicited publish_namespace from a peer that implements MoQ Solicit"); @@ -2082,7 +2082,7 @@ impl Subscriber { async fn run_publish_namespace_updates( &mut self, stream: &mut Stream, - path: &PathOwned, + path: &Path, held: Held, peer: cluster::Peer, attached: &mut Option, @@ -2397,7 +2397,7 @@ impl Subscriber { /// Retract an advertisement's route ahead of a withdrawal that waits on the peer, so a peer /// that stops reading cannot keep it routed. `attached` makes it once only. - fn release_announce(&mut self, path: &PathOwned, attached: &mut Option, token: bool) -> Result<(), Error> { + fn release_announce(&mut self, path: &Path, attached: &mut Option, token: bool) -> Result<(), Error> { if let Some(id) = attached.take() { self.stop_announce(path.clone(), id, token)?; } @@ -2410,7 +2410,7 @@ impl Subscriber { &self, stream: &mut Stream, request_id: RequestId, - path: &PathOwned, + path: &Path, err: &Error, reason: &str, ) -> Result<(), Error> { @@ -2565,7 +2565,7 @@ impl Subscriber { /// this advertisement. /// /// Pair with [`Self::stop_announce`]. - fn start_announce(&mut self, path: PathOwned, advert: Advertised, admit: Admit) -> Result { + fn start_announce(&mut self, path: Path, advert: Advertised, admit: Admit) -> Result { let mut state = self.state.lock(); let (token, slot) = match admit { Admit::Session(slot) => (false, slot), @@ -2596,7 +2596,7 @@ impl Subscriber { /// Attach the route of a namespace the limit withheld, returning the generation /// to serve. `None` when it is attached already. - fn reattach(&self, path: &PathOwned, entry: &mut BroadcastState) -> Result, Error> { + fn reattach(&self, path: &Path, entry: &mut BroadcastState) -> Result, Error> { if entry.dynamic.is_some() { return Ok(None); } @@ -2606,7 +2606,7 @@ impl Subscriber { } entry.dynamic = Some(self.origin.requests_route(path, route)?); entry.generation += 1; - tracing::info!(route = %self.origin.absolute(path), "namespace authorized again"); + tracing::info!(route = %crate::path::Rooted(self.origin.root(), path), "namespace authorized again"); Ok(Some(entry.generation)) } @@ -2614,7 +2614,7 @@ impl Subscriber { /// /// An update replaces the advertisement atomically: the refcount does not move, /// and no subscription is torn down merely because one arrived. - fn update_announce(&mut self, path: PathOwned, id: u64, advert: Advertised) -> Result<(), Error> { + fn update_announce(&mut self, path: Path, id: u64, advert: Advertised) -> Result<(), Error> { let mut state = self.state.lock(); if !state.broadcasts.contains_key(&path) { return Err(Error::NotFound); @@ -2635,7 +2635,7 @@ impl Subscriber { fn attach( &self, state: &mut State, - path: PathOwned, + path: Path, id: u64, advert: Advertised, slot: Option, @@ -2683,7 +2683,7 @@ impl Subscriber { // Outside the session's limit: held, not refused, so a wider limit can // attach it while the peer still advertises it. if !token && !self.auth.within_limit(crate::auth::Direction::Subscribe, path.as_str()) { - tracing::debug!(route = %self.origin.absolute(&path), "withholding announce outside the limit"); + tracing::debug!(route = %crate::path::Rooted(self.origin.root(), &path), "withholding announce outside the limit"); entry.insert(BroadcastState { route: route.clone(), adverts: vec![(id, route)], @@ -2708,7 +2708,7 @@ impl Subscriber { _slot: slot, }); - tracing::debug!(route = %self.origin.absolute(&path), "announce"); + tracing::debug!(route = %crate::path::Rooted(self.origin.root(), &path), "announce"); self.serve_route(path, 0); Ok(()) @@ -2728,7 +2728,7 @@ impl Subscriber { /// Release advertisement `id` of `path`, closing its sources when it was the last. /// `token` says its own request token authorized it. - fn stop_announce(&mut self, path: PathOwned, id: u64, token: bool) -> Result<(), Error> { + fn stop_announce(&mut self, path: Path, id: u64, token: bool) -> Result<(), Error> { let mut state = self.state.lock(); match state.broadcasts.entry(path.clone()) { @@ -2740,7 +2740,7 @@ impl Subscriber { state.adverts.remove(index); state.tokens -= usize::from(token); if state.adverts.is_empty() { - tracing::debug!(route = %self.origin.absolute(&path), "unannounced"); + tracing::debug!(route = %crate::path::Rooted(self.origin.root(), &path), "unannounced"); // Dropping the entry retracts the route (its announcement drops) and // closes its sources (its token closes). entry.remove(); @@ -2759,7 +2759,7 @@ impl Subscriber { && !self.auth.within_limit(crate::auth::Direction::Subscribe, path.as_str()) { // The last token that stood in for the limit is gone: hold the route back. - tracing::info!(route = %self.origin.absolute(&path), "namespace no longer authorized"); + tracing::info!(route = %crate::path::Rooted(self.origin.root(), &path), "namespace no longer authorized"); state.dynamic = None; state.live = Default::default(); } @@ -2790,7 +2790,7 @@ impl Subscriber { /// per requested path and serve its track requests until the route is /// retracted or the session dies. Tracks in flight at a retraction run to /// their own end, unless the announce was what authorized them. - async fn run_route(&self, path: PathOwned, generation: u64) { + async fn run_route(&self, path: Path, generation: u64) { let mut broadcasts = TaskSet::owned(); let mut closed_session = self.session.clone(); let mut epoch = 0; @@ -2829,7 +2829,7 @@ impl Subscriber { } // A namespace its own request token authorized is exempt from the limit. if excluded && entry.tokens == 0 && entry.dynamic.is_some() { - tracing::info!(route = %self.origin.absolute(&path), "namespace no longer authorized"); + tracing::info!(route = %crate::path::Rooted(self.origin.root(), &path), "namespace no longer authorized"); entry.dynamic = None; // Dropping the namespace's token closes every source minted under it. entry.live = Default::default(); @@ -2854,7 +2854,7 @@ impl Subscriber { // The request path is absolute; the wire (and our origin handle) speak // paths relative to the session's root. let requested = match request.path().strip_prefix(self.origin.root()) { - Some(requested) => requested.to_owned(), + Some(requested) => requested.clone(), None => continue, }; // The source serves the instance the route names now: every request for it names @@ -2874,7 +2874,8 @@ impl Subscriber { }); (validity, entry.and_then(|entry| entry.route.epoch.clone())) }; - let source = crate::model::broadcast::SourceGuard::new(self.origin.create_source(&requested, validity)); + let source = + crate::model::broadcast::SourceGuard::new(self.origin.create_source(requested.clone(), validity)); // The handler exists before the requester sees the source, so a track it asks // for before `run_broadcast` first polls queues instead of failing `NotFound`. let dynamic = source.requests(); @@ -2898,7 +2899,7 @@ impl Subscriber { this.validity = dynamic.validity().clone(); this.epoch = epoch; broadcasts.push(async move { - if let Err(err) = this.run_broadcast(requested.borrow(), source, dynamic, route).await { + if let Err(err) = this.run_broadcast(requested, source, dynamic, route).await { tracing::debug!(%err, "error running broadcast"); } }); @@ -2908,7 +2909,7 @@ impl Subscriber { /// Re-price one attached route to a draining cost (the peer sent a GOAWAY): /// every other candidate outranks it while it stays selectable as the last /// path. Idempotent, since the signal stays set. - fn drain_route(&self, path: &PathOwned) { + fn drain_route(&self, path: &Path) { let mut state = self.state.lock(); let Some(entry) = state.broadcasts.get_mut(path) else { return; @@ -2930,7 +2931,7 @@ impl Subscriber { /// selecting its instance, nothing holds it any more, or the session dies. async fn run_broadcast( &self, - path: Path<'_>, + path: Path, source: crate::model::broadcast::SourceGuard, mut broadcast: broadcast::Requests, route: Option>, @@ -2986,7 +2987,7 @@ impl Subscriber { let mut this = self.clone(); - let path = path.to_owned(); + let path = path.clone(); let broadcast = broadcast.clone(); let route = route.clone(); subscribes.push(async move { @@ -3001,7 +3002,7 @@ impl Subscriber { /// it; a failed listing ends its readers, which ask again. fn poll_listing( &self, - path: &Path<'_>, + path: &Path, lister: &std::sync::Arc, listening: &mut Option, waiter: &kio::Waiter, @@ -3017,7 +3018,7 @@ impl Subscriber { lister.abort(Error::Unsupported); continue; } - *listening = Some(self.listen(path.to_owned(), lister.clone())); + *listening = Some(self.listen(path.clone(), lister.clone())); } Some(_) if lister.poll_unused(waiter).is_ready() => { *listening = None; @@ -3036,7 +3037,7 @@ impl Subscriber { async fn run_subscribe( &mut self, - broadcast_path: Path<'_>, + broadcast_path: Path, // Held for the subscription's lifetime but never watched: the broadcast ending // is a retraction, which does not disturb a subscription the scope allows. _broadcast: broadcast::Requests, @@ -3051,7 +3052,7 @@ impl Subscriber { // does, leaving the rest of the session alone. let (auth, path, direction) = ( self.auth.clone(), - broadcast_path.to_owned(), + broadcast_path.clone(), crate::auth::Direction::Subscribe, ); let mut gate = match route { @@ -3132,7 +3133,7 @@ impl Subscriber { /// `None` once it failed. async fn subscribe_once( &mut self, - broadcast_path: &Path<'_>, + broadcast_path: &Path, track_name: &str, target: Target, group_fetches: &mut TaskSet, @@ -3179,7 +3180,7 @@ impl Subscriber { // A listed track's paused subscription is already this track's upstream copy, so // resume it rather than open a second one. - let key: tracks::TrackKey = (broadcast_path.to_owned(), track_name.into()); + let key: tracks::TrackKey = (broadcast_path.clone(), track_name.into()); let adopted = match &self.want(&key) { None => None, Some(handoff) => { @@ -3292,7 +3293,7 @@ impl Subscriber { epoch: self.epoch.clone(), alias, tail, - ..TrackState::pending(track_name.to_owned(), broadcast_path.to_owned(), fill.clone()) + ..TrackState::pending(track_name.to_owned(), broadcast_path.clone(), fill.clone()) }, ); } @@ -3334,7 +3335,7 @@ impl Subscriber { max_age, new_group, request_id, - track_namespace: broadcast_path.to_owned(), + track_namespace: broadcast_path.clone(), track_name: track_name.into(), subscriber_priority: super::priority::to_wire(priority), group_order: order.into(), @@ -3361,7 +3362,7 @@ impl Subscriber { return None; } - tracing::debug!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "subscribe started"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "subscribe started"); // Park the origin request where a session abort can reject it. The producer // does not exist until SUBSCRIBE_OK, and dropping this task would otherwise @@ -3469,7 +3470,7 @@ impl Subscriber { let Some(response) = setup else { tracing::debug!( - broadcast = %self.origin.absolute(broadcast_path), + broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "subscribe abandoned before it was accepted" ); @@ -3509,7 +3510,7 @@ impl Subscriber { { let err = refused(&accepted.properties, self.version, self.peer_setup.get().await.flate) .expect("checked by the guard"); - tracing::debug!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, %err, "subscribe refused"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, %err, "subscribe refused"); // Refuse before waiting on the peer to acknowledge the cancellation. if let Some(pending) = self.take_pending(request_id) { pending.reject(err.clone()); @@ -3935,7 +3936,7 @@ impl Subscriber { } } End::Revoked => { - tracing::info!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "subscription no longer authorized"); + tracing::info!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "subscription no longer authorized"); let _ = track.clone().abort(Error::Unauthorized); failed = true; ended = Some(Err(Error::Unauthorized)); @@ -3943,7 +3944,7 @@ impl Subscriber { } End::Fill(requested) => { let fetch = self.clone().serve_fill( - broadcast_path.to_owned(), + broadcast_path.clone(), track_name.to_owned(), requested, timescale, @@ -3951,7 +3952,7 @@ impl Subscriber { group_fetches.push(fetch); } End::Idle => { - tracing::debug!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "subscribe cancelled (idle)"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "subscribe cancelled (idle)"); break true; } End::Done(res) => { @@ -3964,7 +3965,7 @@ impl Subscriber { ended = Some(end.clone()); match res.map(|done| done.stream_count) { Ok(count) => { - tracing::debug!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "subscribe complete"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "subscribe complete"); // The publisher sends PUBLISH_DONE once every data stream it opened // is closed, but QUIC does not order them, so some can still be on // their way. Wait until Stream Count of their headers arrived and @@ -4103,7 +4104,7 @@ impl Subscriber { /// it through the linger, or the session ended. A finished copy never subscribes again. async fn linger( &mut self, - broadcast_path: &Path<'_>, + broadcast_path: &Path, track_name: &str, mut idle: Idle, group_fetches: &mut TaskSet, @@ -4169,13 +4170,13 @@ impl Subscriber { Step::Fill(fill) => { let fetch = self.clone() - .serve_fill(broadcast_path.to_owned(), track_name.to_owned(), fill, idle.timescale); + .serve_fill(broadcast_path.clone(), track_name.to_owned(), fill, idle.timescale); group_fetches.push(fetch); } Step::Subscribe => break true, Step::Expired => match idle.track.abort_unused(Error::Cancel) { Ok(()) => { - tracing::debug!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "track released (idle)"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "track released (idle)"); self.state.lock().lingering.remove(&id); return None; } @@ -4185,7 +4186,7 @@ impl Subscriber { } }, Step::Revoked => { - tracing::info!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "lingering track no longer authorized"); + tracing::info!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "lingering track no longer authorized"); let _ = idle.track.clone().abort(Error::Unauthorized); self.drain(group_fetches).await; break false; @@ -4560,7 +4561,7 @@ impl Subscriber { /// refused or abandoned: a refused status fails the query-only or fetch-only request. async fn track_status( &mut self, - broadcast_path: &Path<'_>, + broadcast_path: &Path, track_name: &str, request: track::Request, gate: &mut crate::auth::Gate, @@ -4605,7 +4606,7 @@ impl Subscriber { .writer .encode_message(&ietf::TrackStatus { request_id, - track_namespace: broadcast_path.to_owned(), + track_namespace: broadcast_path.clone(), track_name: track_name.into(), properties_wanted: true, epoch: self.epoch.clone(), @@ -4678,13 +4679,13 @@ impl Subscriber { let ok = match ok { Ok(ok) => ok, Err(err) => { - tracing::debug!(%err, broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "track status refused"); + tracing::debug!(%err, broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "track status refused"); request.reject(err); return None; } }; - tracing::debug!(broadcast = %self.origin.absolute(broadcast_path), track = %track_name, "track status known"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), broadcast_path), track = %track_name, "track status known"); let dynamic = request.requests(); // Nothing feeds this copy live: what it caches is fetched, never the live edge. Idle // from the accept, so no reader takes it as live in between. @@ -5666,7 +5667,7 @@ impl Subscriber { /// group always goes alone: a run including the group its source is still writing would /// hold the earlier groups until it finishes, or be cut short at the Largest Object by a /// publisher following draft-22 section 9.11. - async fn serve_fill(self, broadcast: PathOwned, name: String, fill: track::Fill, timescale: Option) { + async fn serve_fill(self, broadcast: Path, name: String, fill: track::Fill, timescale: Option) { let fill = std::sync::Arc::new(fill); let mut last = None; while fill.is_used() @@ -5700,13 +5701,7 @@ impl Subscriber { /// Serve a whole fill with one FETCH, as [`Self::serve_fill`] serves each run. #[cfg(test)] - async fn run_group_fetch( - self, - broadcast: PathOwned, - name: String, - fill: track::Fill, - timescale: Option, - ) { + async fn run_group_fetch(self, broadcast: Path, name: String, fill: track::Fill, timescale: Option) { let run = fill.start().group..fill.end(); self.run_range_fetch(broadcast, name, std::sync::Arc::new(fill), run, timescale) .await; @@ -5722,7 +5717,7 @@ impl Subscriber { /// fetch stream, which [`Self::recv_fill`] routes to [`Self::recv_group_fetch`]. async fn run_range_fetch( self, - broadcast: PathOwned, + broadcast: Path, name: String, fill: std::sync::Arc, run: std::ops::Range, @@ -6718,7 +6713,7 @@ mod tests { pending: Some(request), ..TrackState::pending( "video".to_string(), - crate::Path::new("bcast").to_owned(), + crate::Path::from_static("bcast"), kio::Producer::new(Fill::Done), ) }, @@ -6871,7 +6866,7 @@ mod tests { let mut namespaces = crate::model::interest_prefixes(&subscriber.origin.allowed()); assert_eq!( namespaces, - vec![crate::Path::new("cam").to_owned()], + vec![crate::Path::from_static("cam")], "one SUBSCRIBE_NAMESPACE per permitted prefix, relative to the root", ); let prefix = namespaces.pop().unwrap(); @@ -6911,7 +6906,7 @@ mod tests { writer.varint(ietf::Namespace::ID).await.unwrap(); writer .encode(&ietf::Namespace { - suffix: crate::Path::new(suffix), + suffix: crate::Path::new(suffix).unwrap(), params: None, }) .await @@ -7020,7 +7015,7 @@ mod tests { writer.varint(ietf::Namespace::ID).await.unwrap(); writer .encode(&ietf::Namespace { - suffix: crate::Path::new("room"), + suffix: crate::Path::from_static("room"), params: Some(ietf::AdvertParams { cluster: Some(cluster::Advert { hops, cost: 0 }), epoch, @@ -7103,7 +7098,7 @@ mod tests { .await .unwrap(); let namespace = |suffix| ietf::Namespace { - suffix: crate::Path::new(suffix), + suffix: crate::Path::new(suffix).unwrap(), params: Some(ietf::AdvertParams { cluster: Some(cluster::Advert { hops: hop_path(&[7, 5, 9]), @@ -7117,7 +7112,7 @@ mod tests { writer.varint(ietf::NamespaceDone::ID).await.unwrap(); writer .encode(&ietf::NamespaceDone { - suffix: crate::Path::new("a"), + suffix: crate::Path::from_static("a"), }) .await .unwrap(); @@ -7229,7 +7224,7 @@ mod tests { let res = subscriber .run_subscribe_namespace( stream, - crate::Path::new("").to_owned(), + crate::Path::from_static(""), crate::model::Snapshot::landed(), live, ) @@ -7264,7 +7259,7 @@ mod tests { writer.varint(ietf::NamespaceDone::ID).await.unwrap(); writer .encode(&ietf::NamespaceDone { - suffix: crate::Path::new(suffix), + suffix: crate::Path::new(suffix).unwrap(), }) .await .unwrap(); @@ -7273,7 +7268,7 @@ mod tests { writer.varint(ietf::Namespace::ID).await.unwrap(); writer .encode(&ietf::Namespace { - suffix: crate::Path::new(entry), + suffix: crate::Path::new(entry).unwrap(), params: None, }) .await @@ -7332,7 +7327,7 @@ mod tests { .unwrap(); let mut run = std::pin::pin!(subscriber.run_subscribe_namespace( stream, - crate::Path::new("").to_owned(), + crate::Path::from_static(""), snapshot, live )); @@ -7360,7 +7355,7 @@ mod tests { .await .unwrap(); let mut run = - std::pin::pin!(subscriber.run_subscribe_namespace(stream, crate::Path::new("").to_owned(), snapshot, live)); + std::pin::pin!(subscriber.run_subscribe_namespace(stream, crate::Path::from_static(""), snapshot, live)); for _ in 0..10 { assert!(futures::poll!(run.as_mut()).is_pending()); settle().await; @@ -7384,7 +7379,7 @@ mod tests { .await .unwrap(); let res = subscriber - .run_subscribe_namespace(stream, crate::Path::new("").to_owned(), snapshot, live) + .run_subscribe_namespace(stream, crate::Path::from_static(""), snapshot, live) .await; assert!(matches!(res, Err(Error::ProtocolViolation)), "{entries:?}: {res:?}"); } @@ -7506,7 +7501,7 @@ mod tests { *name, crate::track::Info::timed(), ), - Path::new(broadcast).to_owned(), + Path::new(broadcast).unwrap(), kio::Producer::new(Fill::Done), ), ); @@ -7786,7 +7781,7 @@ mod tests { let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); @@ -7852,7 +7847,7 @@ mod tests { let mut serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); assert!( @@ -7893,7 +7888,7 @@ mod tests { let live = route.consume(); let mut serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, Some(live), request) + .run_subscribe(Path::from_static("broadcast"), dynamic, Some(live), request) .await; }); assert!( @@ -7952,7 +7947,7 @@ mod tests { let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); @@ -8023,7 +8018,7 @@ mod tests { let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); settle().await; @@ -8089,7 +8084,7 @@ mod tests { let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); // Let the SUBSCRIBE go out. No SUBSCRIBE_OK is coming. @@ -8156,7 +8151,7 @@ mod tests { let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); @@ -8224,7 +8219,7 @@ mod tests { subscriber.validity = crate::model::broadcast::RequestValidity::token(Some(route.consume())); let mut gate = crate::auth::Gate::new( auth.clone(), - Path::new("broadcast").to_owned(), + Path::from_static("broadcast"), crate::auth::Direction::Subscribe, ); let waiter = kio::Waiter::noop(); @@ -8244,7 +8239,7 @@ mod tests { assert!( subscriber .subscribe_once( - &Path::new("broadcast"), + &Path::from_static("broadcast"), "video", Target::Request(request), &mut TaskSet::owned(), @@ -8257,7 +8252,7 @@ mod tests { SetupWait::StatusOpen | SetupWait::StatusCredit | SetupWait::StatusResponse => Box::pin(async { assert!( subscriber - .track_status(&Path::new("broadcast"), "video", request, &mut gate) + .track_status(&Path::from_static("broadcast"), "video", request, &mut gate) .await .is_none() ); @@ -8271,7 +8266,7 @@ mod tests { group = Some(pending); accepted = Some(track); Box::pin(subscriber.clone().run_group_fetch( - Path::new("broadcast").to_owned(), + Path::from_static("broadcast"), "video".into(), request, None, @@ -8435,18 +8430,18 @@ mod tests { let serving = match kind { Allocated::Subscribe => moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("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(), + Path::from_static("broadcast"), crate::auth::Direction::Subscribe, ); assert!( subscriber - .track_status(&Path::new("broadcast"), "video", request, &mut gate) + .track_status(&Path::from_static("broadcast"), "video", request, &mut gate) .await .is_none() ); @@ -8463,7 +8458,7 @@ mod tests { let group = requested.requested_fill().await.expect("group request"); fetch = Some((track, pending)); moq_clock::sim::spawn(subscriber.run_group_fetch( - Path::new("broadcast").to_owned(), + Path::from_static("broadcast"), "video".into(), group, None, @@ -8574,7 +8569,7 @@ mod tests { let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); @@ -8646,7 +8641,7 @@ mod tests { 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) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); settle().await; @@ -8725,7 +8720,7 @@ mod tests { 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) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); settle().await; @@ -8846,7 +8841,7 @@ mod tests { "video", crate::track::Info::timed(), ), - Path::new("broadcast").to_owned(), + Path::from_static("broadcast"), kio::Producer::new(Fill::Done), ) }, @@ -8869,7 +8864,7 @@ mod tests { let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); @@ -8952,7 +8947,7 @@ mod tests { let probe = subscriber.clone(); let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); @@ -9074,7 +9069,7 @@ mod tests { Default::default(), ); subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); // Run the request classification before the front can add demand after @@ -9174,7 +9169,7 @@ mod tests { assert!(futures::poll!(pending.as_mut()).is_pending()); let request = dynamic.requested_track().await.expect("no track requested"); let mut serving = - Box::pin(subscriber.run_subscribe(Path::new("broadcast"), dynamic, None, request)); + Box::pin(subscriber.run_subscribe(Path::from_static("broadcast"), dynamic, None, request)); assert!( futures::poll!(serving.as_mut()).is_ready(), "{version}: refusal was not terminal" @@ -9258,7 +9253,7 @@ mod tests { let probe = subscriber.clone(); let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); settle().await; @@ -9371,8 +9366,8 @@ mod tests { subscriber.announces = crate::session::Slots::new(1); let advert = |subscriber: &Subscriber| subscriber.route(None, &cluster::Peer::default()).expect("route"); - let a = crate::Path::new("a").to_owned(); - let b = crate::Path::new("b").to_owned(); + let a = crate::Path::from_static("a"); + let b = crate::Path::from_static("b"); let (first, repeat, over, last) = ( advert(&subscriber), advert(&subscriber), @@ -9429,7 +9424,7 @@ mod tests { let advert = subscriber.route(None, &cluster::Peer::default()).expect("route"); subscriber - .start_announce(crate::Path::new("room/host").to_owned(), advert, Admit::Session(None)) + .start_announce(crate::Path::from_static("room/host"), advert, Admit::Session(None)) .unwrap(); let mut announced = consumer.announced(); @@ -9489,7 +9484,7 @@ mod tests { ); let advert = subscriber.route(None, &cluster::Peer::default()).expect("route"); subscriber - .start_announce(crate::Path::new("room/host").to_owned(), advert, Admit::Session(None)) + .start_announce(crate::Path::from_static("room/host"), advert, Admit::Session(None)) .unwrap(); // No announce churn: the upstream route stays the best one, on both cursors. @@ -9529,7 +9524,7 @@ mod tests { ); let advert = subscriber.route(None, &cluster::Peer::default()).expect("route"); subscriber - .start_announce(crate::Path::new("room/host").to_owned(), advert, Admit::Session(None)) + .start_announce(crate::Path::from_static("room/host"), advert, Admit::Session(None)) .unwrap(); subscriber }; @@ -9617,11 +9612,7 @@ mod tests { let mut subscriber = subscriber; subscriber - .start_announce( - crate::Path::new("room/host").to_owned(), - advertised, - Admit::Session(None), - ) + .start_announce(crate::Path::from_static("room/host"), advertised, Admit::Session(None)) .unwrap(); let route = routed_now(&consumer, "room/host").expect("routed"); @@ -9722,7 +9713,7 @@ mod tests { subscriber .run_subscribe_namespace( stream, - crate::Path::new("").to_owned(), + crate::Path::from_static(""), crate::model::Snapshot::landed(), live, ) @@ -9765,7 +9756,7 @@ mod tests { writer.varint(ietf::Namespace::ID).await.unwrap(); writer .encode(&ietf::Namespace { - suffix: crate::Path::new("x.hang"), + suffix: crate::Path::from_static("x.hang"), params: Some(ietf::AdvertParams { cluster: Some(cluster::Advert { hops: hop_path(&[7, 9]), @@ -9812,7 +9803,7 @@ mod tests { .unwrap(); let mut run = std::pin::pin!(subscriber.run_subscribe_namespace( stream, - crate::Path::new("").to_owned(), + crate::Path::from_static(""), crate::model::Snapshot::landed(), live )); @@ -9838,7 +9829,7 @@ mod tests { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let advert = subscriber.route(None, &cluster::Peer::default()).expect("route"); let id = subscriber .start_announce(path.clone(), advert, Admit::Session(None)) @@ -9882,7 +9873,7 @@ mod tests { .unwrap(); let msg = ietf::PublishNamespace { request_id: RequestId(0), - track_namespace: crate::Path::new("room/host"), + track_namespace: crate::Path::from_static("room/host"), params: Default::default(), authorization_token: None, }; @@ -9911,7 +9902,7 @@ mod tests { 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 path = crate::Path::from_static("room/host"); 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())), @@ -9919,7 +9910,7 @@ mod tests { ); writer .encode_message(&ietf::PublishNamespaceDone { - track_namespace: path.borrow(), + track_namespace: path.clone(), request_id: RequestId(0), }) .await @@ -9950,7 +9941,7 @@ mod tests { .unwrap(); let msg = ietf::PublishNamespace { request_id: RequestId(0), - track_namespace: path.borrow(), + track_namespace: path.clone(), params: Default::default(), authorization_token: None, }; @@ -9972,7 +9963,7 @@ mod tests { #[moq_clock::test] async fn a_legacy_update_on_an_advertisement_is_answered() { for version in [Version::Draft14, Version::Draft15] { - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); // 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 { @@ -9988,7 +9979,7 @@ mod tests { ); writer .encode_message(&ietf::PublishNamespaceDone { - track_namespace: path.borrow(), + track_namespace: path.clone(), request_id: RequestId(0), }) .await @@ -10018,7 +10009,7 @@ mod tests { .unwrap(); let msg = ietf::PublishNamespace { request_id: RequestId(0), - track_namespace: path.borrow(), + track_namespace: path.clone(), params: Default::default(), authorization_token: None, }; @@ -10085,13 +10076,13 @@ mod tests { Default::default(), ); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); 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(), + track_namespace: path.clone(), params: Default::default(), authorization_token: None, }; @@ -10120,7 +10111,7 @@ mod tests { async fn the_last_owner_out_decides_the_detach() { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let peer = cluster::Peer::default(); let mut ids = Vec::new(); @@ -10158,11 +10149,13 @@ mod tests { async fn a_minted_source_closes_unheld_or_retracted() { let (subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let run = |route: &kio::Producer<()>| { - let source = crate::model::broadcast::SourceGuard::new(origin.create_source("pool/p", Default::default())); + let source = crate::model::broadcast::SourceGuard::new( + origin.create_source(crate::Path::from_static("pool/p"), Default::default()), + ); let (holder, dynamic) = (source.consume(), source.requests()); let (this, route) = (subscriber.clone(), route.consume()); let task = moq_clock::sim::spawn(async move { - this.run_broadcast(crate::Path::new("pool/p"), source, dynamic, Some(route)) + this.run_broadcast(crate::Path::from_static("pool/p"), source, dynamic, Some(route)) .await }); (holder, task) @@ -10217,7 +10210,7 @@ mod tests { async fn cluster_update_replaces_in_place() { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let first = Advertised { route: crate::origin::Route::default() @@ -10258,7 +10251,7 @@ mod tests { async fn anonymous_publisher_survives_a_repricing_update() { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); // A free link, so the route cost is exactly what the peer advertised. let peer = cluster::Peer { hop: Some(crate::Hop::new(9).unwrap()), @@ -10308,7 +10301,7 @@ mod tests { async fn a_reordered_replacement_keeps_the_new_instance() { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let peer = cluster::Peer::default(); let (old, new) = (crate::Epoch::mint(), crate::Epoch::mint()); let advert = |subscriber: &Subscriber, epoch: &crate::Epoch| { @@ -10356,7 +10349,7 @@ mod tests { async fn a_repeat_under_another_epoch_becomes_the_newest() { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let peer = cluster::Peer::default(); let (old, new) = (crate::Epoch::mint(), crate::Epoch::mint()); let advert = |subscriber: &Subscriber, epoch: &crate::Epoch| { @@ -10398,7 +10391,7 @@ mod tests { async fn separate_adverts_refcount_the_route() { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let peer = cluster::Peer { hop: Some(crate::Hop::new(9).unwrap()), cost: None, @@ -10436,7 +10429,7 @@ mod tests { async fn pathless_adverts_never_replace_the_source() { let (mut subscriber, origin) = cluster_subscriber(crate::Hop::new(1).unwrap()); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let peer = cluster::Peer::default(); // What a PUBLISH_NAMESPACE with no cluster parameters resolves to. @@ -10468,7 +10461,7 @@ mod tests { let self_origin = crate::Hop::new(5).unwrap(); let (mut subscriber, origin) = cluster_subscriber(self_origin); let consumer = origin.consume(); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let peer = cluster::Peer { hop: Some(crate::Hop::new(9).unwrap()), cost: None, @@ -10568,7 +10561,7 @@ mod tests { Default::default(), ); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let advert = subscriber.route(Some(attached), peer).expect("route"); let id = subscriber.start_announce(path, advert, Admit::Session(None)).unwrap(); assert_eq!(id, 0, "the harness advertisement is the first"); @@ -10655,7 +10648,7 @@ mod tests { let err = subscriber .run_publish_namespace_updates( &mut stream, - &crate::Path::new("room/host"), + &crate::Path::from_static("room/host"), Held { cluster: Some(clean), epoch: None, @@ -10684,7 +10677,7 @@ mod tests { let (mut subscriber, consumer, mut stream, log) = reflected_harness(self_origin, &peer, &clean, &[looped]).await; - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let mut attached = Some(0); { let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates( @@ -10734,7 +10727,7 @@ mod tests { let (mut subscriber, consumer, mut stream, _log) = reflected_harness(self_origin, &peer, &clean, &[looped, clean.clone()]).await; - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let mut attached = Some(0); { let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates( @@ -10797,7 +10790,7 @@ mod tests { subscriber.announces = crate::session::Slots::new(1); // Admitted by its own token, which holds the one slot for the request's life. - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let hold: crate::auth::Hold = std::sync::Arc::new(subscriber.announces.acquire().unwrap()); let auth = crate::auth::Link::new(true); let mut requests = auth.handle().requests(); @@ -10898,7 +10891,7 @@ mod tests { std::mem::forget(driver); assert_eq!(routed_now(&consumer, "room/host").expect("routed").cost.value(), 4); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let mut attached = Some(0); { let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates( @@ -10949,7 +10942,7 @@ mod tests { // repriced, which is the one way an in-place update fails. drop(driver); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let mut attached = Some(0); let mut result = None; { @@ -11004,7 +10997,7 @@ mod tests { update_harness(self_origin, &peer, &clean, script).await; std::mem::forget(driver); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let mut attached = Some(0); { let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates( @@ -11054,7 +11047,7 @@ mod tests { writer .encode(&ietf::PublishNamespace { request_id: RequestId(1), - track_namespace: crate::Path::new("room/host"), + track_namespace: crate::Path::from_static("room/host"), params: ietf::AdvertParams { cluster: Some(cluster::Advert { cost: 0, @@ -11074,7 +11067,7 @@ mod tests { update_harness(self_origin, &peer, &clean, script).await; std::mem::forget(driver); - let path = crate::Path::new("room/host").to_owned(); + let path = crate::Path::from_static("room/host"); let mut attached = Some(0); let mut run = std::pin::pin!(subscriber.run_publish_namespace_updates( &mut stream, @@ -11112,7 +11105,7 @@ mod tests { let mut live = std::collections::HashMap::new(); for path in ["room/a", "room/b"] { - let path = crate::Path::new(path).to_owned(); + let path = crate::Path::new(path).unwrap(); let advert = subscriber.route(None, &peer).expect("route"); let id = subscriber .start_announce(path.clone(), advert, Admit::Session(None)) @@ -11167,7 +11160,7 @@ mod tests { .unwrap(); let msg = ietf::Publish { request_id: RequestId(1), - track_namespace: crate::Path::new("room/host"), + track_namespace: crate::Path::from_static("room/host"), track_name: "video".into(), track_alias: 7, largest_location: None, @@ -11333,10 +11326,10 @@ mod tests { }) } - fn token_publish_namespace() -> ietf::PublishNamespace<'static> { + fn token_publish_namespace() -> ietf::PublishNamespace { ietf::PublishNamespace { request_id: RequestId(1), - track_namespace: crate::Path::new("room/alice"), + track_namespace: crate::Path::from_static("room/alice"), params: Default::default(), authorization_token: Some(announce_token()), } @@ -11525,7 +11518,7 @@ mod tests { VERSION, ) .encode_message(&ietf::PublishNamespaceDone { - track_namespace: crate::Path::new("room/alice"), + track_namespace: crate::Path::from_static("room/alice"), request_id: RequestId(1), }) .await @@ -12122,7 +12115,7 @@ mod tests { ); writer .encode_message(&ietf::PublishNamespaceCancel { - track_namespace: crate::Path::new("room/alice"), + track_namespace: crate::Path::from_static("room/alice"), request_id: RequestId(1), error_code: request::to_code( &Error::Unauthorized, @@ -12156,7 +12149,7 @@ mod tests { .unwrap(); let tokenless = ietf::PublishNamespace { request_id: RequestId(1), - track_namespace: crate::Path::new("other/alice"), + track_namespace: crate::Path::from_static("other/alice"), params: Default::default(), authorization_token: None, }; @@ -13598,7 +13591,7 @@ mod stitch_tests { TrackState { alias: Some(ALIAS), timescale, - ..TrackState::new(track.clone(), Path::new("broadcast").to_owned(), fill.clone()) + ..TrackState::new(track.clone(), Path::from_static("broadcast"), fill.clone()) }, ); insert_track_alias(&state.aliases, ALIAS, REQUEST).unwrap(); @@ -13942,7 +13935,7 @@ mod stitch_tests { let id = if let Some((start, end)) = fetch { let fetch_type = if Filter::is_draft22(egress) { ietf::FetchType::Filtered { - namespace: Path::new("room"), + namespace: Path::from_static("room"), track: "video".into(), filter: Filter::Absolute { start: ietf::Location { @@ -13957,7 +13950,7 @@ mod stitch_tests { } } else { ietf::FetchType::Standalone { - namespace: Path::new("room"), + namespace: Path::from_static("room"), track: "video".into(), start: ietf::Location { group: SEQUENCE, @@ -13987,7 +13980,7 @@ mod stitch_tests { max_age: None, new_group: None, request_id: RequestId(3), - track_namespace: Path::new("room"), + track_namespace: Path::from_static("room"), track_name: "video".into(), subscriber_priority: 128, group_order: ietf::GroupOrder::Ascending, @@ -14700,7 +14693,7 @@ mod stitch_tests { max_age: None, new_group: None, request_id: RequestId(3), - track_namespace: Path::new("room"), + track_namespace: Path::from_static("room"), track_name: "video".into(), subscriber_priority: 128, group_order: ietf::GroupOrder::Ascending, @@ -14809,7 +14802,7 @@ mod stitch_tests { max_age: None, new_group: None, request_id: RequestId(3), - track_namespace: Path::new("room"), + track_namespace: Path::from_static("room"), track_name: "video".into(), subscriber_priority: 128, group_order: ietf::GroupOrder::Ascending, @@ -16197,7 +16190,7 @@ mod stitch_tests { track.finish().unwrap(); // The FETCH a relay sends for the run 3..=6. - let namespace = Path::new("room"); + let namespace = Path::from_static("room"); let fetch_type = match Filter::is_draft22(version) { true => ietf::FetchType::Filtered { namespace, @@ -16676,7 +16669,7 @@ mod joining_fetch_tests { let mut serving_subscriber = subscriber.clone(); let serving = moq_clock::sim::spawn(async move { serving_subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); @@ -16764,7 +16757,7 @@ mod joining_fetch_tests { RequestId(99), TrackState::new( track.clone(), - Path::new("broadcast").to_owned(), + Path::from_static("broadcast"), kio::Producer::new(Fill::Requested), ), ); @@ -16801,7 +16794,8 @@ mod joining_fetch_tests { let track = consumer.track("video").unwrap(); let mut subscribing = Box::pin(track.subscribe(None)); let request = dynamic.requested_track().await.unwrap(); - let mut serving = Box::pin(subscriber.run_subscribe(Path::new("broadcast"), dynamic, None, request)); + let mut serving = + Box::pin(subscriber.run_subscribe(Path::from_static("broadcast"), dynamic, None, request)); assert!(futures::poll!(serving.as_mut()).is_pending()); let received = (&mut subscribing).now_or_never().expect("SUBSCRIBE_OK").unwrap(); let mut fetch = Box::pin(track.fetch_group(4, None)); @@ -17281,7 +17275,7 @@ mod joining_fetch_tests { let request = dynamic.requested_fill().await.expect("group request"); let outcome = request.joined(); let mut run = Box::pin(subscriber.clone().run_group_fetch( - Path::new("broadcast").to_owned(), + Path::from_static("broadcast"), "video".into(), request, None, @@ -17563,7 +17557,7 @@ mod joining_fetch_tests { subscriber .clone() - .run_group_fetch(Path::new("broadcast").to_owned(), "video".into(), request, None) + .run_group_fetch(Path::from_static("broadcast"), "video".into(), request, None) .await; assert!(fetch.await.is_err(), "the accepted group was aborted"); @@ -17635,7 +17629,7 @@ mod joining_fetch_tests { subscriber .clone() - .run_group_fetch(Path::new("broadcast").to_owned(), "video".into(), request, None) + .run_group_fetch(Path::from_static("broadcast"), "video".into(), request, None) .await; assert_eq!( @@ -17776,7 +17770,7 @@ mod joining_fetch_tests { let request = dynamic.requested_fill().await.expect("no group requested"); let serving = moq_clock::sim::spawn(subscriber.clone().run_group_fetch( - Path::new("broadcast").to_owned(), + Path::from_static("broadcast"), "video".into(), request, None, @@ -17891,7 +17885,7 @@ mod new_group_tests { let probe = subscriber.clone(); let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); let relay = Relay { @@ -18232,7 +18226,7 @@ mod track_property_tests { let probe = subscriber.clone(); let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("broadcast"), dynamic, None, request) + .run_subscribe(Path::from_static("broadcast"), dynamic, None, request) .await; }); diff --git a/rs/moq-net/src/ietf/subscriber/namespaces.rs b/rs/moq-net/src/ietf/subscriber/namespaces.rs index 15b8f03f347..1e855d4bc1a 100644 --- a/rs/moq-net/src/ietf/subscriber/namespaces.rs +++ b/rs/moq-net/src/ietf/subscriber/namespaces.rs @@ -26,7 +26,7 @@ pub(super) enum Ended { /// A request from when discovery opens it until it is off the wire. struct Request { - prefix: PathOwned, + prefix: Path, /// Dropped to end it; `None` once the session did. live: Option>, /// Closes once it is off the wire, so an overlapping one may go out. @@ -59,7 +59,7 @@ impl Subscriber { }; let mut requests: HashMap = HashMap::new(); // The request discovery holds open for each prefix. - let mut open_at: HashMap = HashMap::new(); + let mut open_at: HashMap = HashMap::new(); let mut running = futures::stream::FuturesUnordered::new(); let mut next = 0u64; @@ -147,7 +147,7 @@ impl Subscriber { async fn namespace_request( mut self, key: u64, - prefix: PathOwned, + prefix: Path, snapshot: crate::model::Snapshot, pending: Pending, ) -> (u64, Result) diff --git a/rs/moq-net/src/ietf/subscriber/tracks.rs b/rs/moq-net/src/ietf/subscriber/tracks.rs index 1bf0899eeae..ed0595000fc 100644 --- a/rs/moq-net/src/ietf/subscriber/tracks.rs +++ b/rs/moq-net/src/ietf/subscriber/tracks.rs @@ -22,11 +22,10 @@ use crate::driver::Transport; use std::sync::Arc; use super::*; -use crate::AsPath as _; use crate::ietf::request_stream::{self, Framed, Messages}; /// A listed track, by its namespace and name. -pub(super) type TrackKey = (PathOwned, Arc); +pub(super) type TrackKey = (Path, Arc); /// How a PUBLISH_DONE leaves the listing: only TRACK_ENDED removes the track. Any other /// status, INTERNAL_ERROR included, fails the listing, so it asks again rather than hide a @@ -89,7 +88,7 @@ impl Drop for Entry { /// A broadcast listed through this session. struct Listener { - path: PathOwned, + path: Path, lister: Arc, /// Set to the error that fails the listing. failed: kio::Producer>, @@ -97,7 +96,7 @@ struct Listener { /// One SUBSCRIBE_TRACKS, from when it is wanted until the peer has closed it. struct Request { - prefix: PathOwned, + prefix: Path, /// Dropped to cancel it; `None` once cancelled, while it leaves the wire. live: Option>, /// Closes once it is off the wire, so an overlapping one may go out. @@ -116,7 +115,7 @@ pub(super) struct Listings { /// A SUBSCRIBE_TRACKS for its task to send. struct Opening { key: u64, - prefix: PathOwned, + prefix: Path, /// The overlapping requests that must leave the wire first. blockers: Vec>, live: kio::Consumer<()>, @@ -130,14 +129,14 @@ impl Listings { } /// The standing request that covers `namespace`; at most one does. - fn covering(&self, namespace: &PathOwned) -> Option { + fn covering(&self, namespace: &Path) -> Option { self.requests .iter() .find(|(_, request)| request.live.is_some() && namespace.has_prefix(&request.prefix)) .map(|(key, _)| *key) } - fn listers<'a>(&'a self, namespace: &'a PathOwned) -> impl Iterator + 'a { + fn listers<'a>(&'a self, namespace: &'a Path) -> impl Iterator + 'a { self.listeners .values() .filter(move |listener| &listener.path == namespace) @@ -146,7 +145,7 @@ impl Listings { fn listen( &mut self, - path: PathOwned, + path: Path, lister: Arc, ) -> (u64, kio::Consumer>, Option) { // What a broader listing already holds is listed at once. @@ -224,7 +223,7 @@ impl Listings { request.live = None; } } - let standing: Vec = self + let standing: Vec = self .requests .values() .filter(|request| request.live.is_some()) @@ -236,7 +235,7 @@ impl Listings { /// Fail every listing the request covering `namespace` serves with `err`, and cancel it: /// they ask again rather than miss a track that may still be live. - fn fail(&mut self, namespace: &PathOwned, err: Error) { + fn fail(&mut self, namespace: &Path, err: Error) { let Some(key) = self.covering(namespace) else { return; }; @@ -348,7 +347,7 @@ enum Offer { impl Subscriber { /// List `path`'s tracks into `lister` until the returned guard drops, through a /// SUBSCRIBE_TRACKS covering it. - pub(super) fn listen(&self, path: PathOwned, lister: Arc) -> Listening { + pub(super) fn listen(&self, path: Path, lister: Arc) -> Listening { let (key, failed, opening) = self.listings.lock().listen(path, lister); if let Some(opening) = opening { let this = self.clone(); @@ -390,7 +389,7 @@ impl Subscriber { async fn subscribe_tracks( &self, - prefix: &PathOwned, + prefix: &Path, blockers: Vec>, live: &kio::Consumer<()>, ) -> Result<(), Error> { @@ -433,7 +432,7 @@ impl Subscriber { ietf::SubscribeTracks::ID, &ietf::SubscribeTracks { request_id, - prefix: prefix.as_path(), + prefix: prefix.clone(), forward: false, subscriber_priority: None, group_order: None, @@ -445,7 +444,7 @@ impl Subscriber { authorization_token: None, }, )?; - tracing::debug!(prefix = %self.origin.absolute(prefix), id = %request_id, "subscribe_tracks started"); + tracing::debug!(prefix = %crate::path::Rooted(self.origin.root(), prefix), id = %request_id, "subscribe_tracks started"); let mut messages = Messages::new(self.session.clone(), self.control.client()); let mut established = false; @@ -498,7 +497,9 @@ impl Subscriber { let skipped = body .message::(self.version) .map_err(|err| messages.failed(err.into()))?; - let namespace = prefix.join(&skipped.suffix); + let namespace = prefix + .join(&skipped.suffix) + .map_err(|err| messages.failed(err.into()))?; self.skipped(namespace, skipped.track_name.as_ref().into()); } _ => return Err(messages.failed(Error::UnexpectedMessage)), @@ -508,7 +509,7 @@ impl Subscriber { // Cancelled: close our half (FIN on draft-18, RESET_STREAM from draft-22, where a // FIN is not a cancel), then read until the peer closes its own. The PUBLISHes it // made stay ours. - tracing::debug!(prefix = %self.origin.absolute(prefix), "subscribe_tracks cancelled"); + tracing::debug!(prefix = %crate::path::Rooted(self.origin.root(), prefix), "subscribe_tracks cancelled"); let Stream { mut writer, mut reader } = stream; match request_stream::fin_cancels(self.version) { true => { @@ -533,7 +534,7 @@ impl Subscriber { } /// A fresh subscription of ours to `name` in `namespace`, and its info once accepted. - fn subscribed(&self, namespace: &PathOwned, name: &str) -> Option<(RequestId, Option)> { + fn subscribed(&self, namespace: &Path, name: &str) -> Option<(RequestId, Option)> { let state = self.state.lock(); let aliases = state.aliases.read(); // A paused one is on its way out with the entry it held. @@ -551,7 +552,7 @@ impl Subscriber { /// The peer skipped a listed track: list it through a subscription of ours, opening /// one paused if we have none. - fn skipped(&self, namespace: PathOwned, name: Arc) { + fn skipped(&self, namespace: Path, name: Arc) { let key = (namespace, name); let mut listings = self.listings.lock(); if listings.entries.contains_key(&key) || listings.covering(&key.0).is_none() { @@ -638,7 +639,7 @@ impl Subscriber { max_age: None, new_group: None, request_id, - track_namespace: key.0.as_path(), + track_namespace: key.0.clone(), track_name: key.1.as_ref().into(), subscriber_priority: super::super::priority::DEFAULT_WIRE, // No update can change it, so it takes the order a reader gets by default. @@ -749,7 +750,7 @@ impl Subscriber { mut stream: Stream, msg: ietf::Publish<'_>, ) -> Result<(), Error> { - let key: TrackKey = (msg.track_namespace.to_owned(), msg.track_name.as_ref().into()); + let key: TrackKey = (msg.track_namespace.clone(), msg.track_name.as_ref().into()); let info = accepted_info(&msg.properties, self.version); let flate = self.peer_setup.get().await.flate; let offer = { @@ -877,7 +878,7 @@ impl Subscriber { } return Ok(()); } - tracing::debug!(broadcast = %self.origin.absolute(&key.0), track = %key.1, id = %request_id, "holding publish"); + tracing::debug!(broadcast = %crate::path::Rooted(self.origin.root(), &key.0), track = %key.1, id = %request_id, "holding publish"); let upstream = Upstream { stream, messages: Messages::new(self.session.clone(), self.control.client()), @@ -1280,7 +1281,7 @@ mod tests { let broadcast = crate::broadcast::Info::default().produce(); let lister = Arc::new(broadcast.requests().lister().unwrap()); let tracks = broadcast.consume().tracks().unwrap(); - let listening = subscriber.listen(PathOwned::from("bcast"), lister); + let listening = subscriber.listen(crate::Path::from_static("bcast"), lister); Harness { subscriber, peer, @@ -1371,7 +1372,7 @@ mod tests { let mut subscriber = self.subscriber.clone(); let serving = moq_clock::sim::spawn(async move { subscriber - .run_subscribe(Path::new("bcast"), dynamic, None, request) + .run_subscribe(Path::from_static("bcast"), dynamic, None, request) .await; }); (track, reader, serving) @@ -1448,7 +1449,7 @@ mod tests { fn offered<'a>(namespace: &'a str, name: &'a str, request_id: u64) -> ietf::Publish<'a> { ietf::Publish { request_id: RequestId(request_id), - track_namespace: Path::new(namespace), + track_namespace: Path::new(namespace).unwrap(), track_name: name.into(), track_alias: request_id, largest_location: None, @@ -1563,7 +1564,7 @@ mod tests { let mut h = harness(version); let mut listing = h.establish().await; let skipped = ietf::PublishSkipped { - suffix: Path::new(""), + suffix: Path::from_static(""), track_name: "audio".into(), }; write(&mut listing, ietf::PublishSkipped::ID, &skipped).await; @@ -1608,7 +1609,7 @@ mod tests { let mut h = harness(version); let mut listing = h.establish().await; let skipped = ietf::PublishSkipped { - suffix: Path::new(""), + suffix: Path::from_static(""), track_name: "audio".into(), }; write(&mut listing, ietf::PublishSkipped::ID, &skipped).await; diff --git a/rs/moq-net/src/ietf/track.rs b/rs/moq-net/src/ietf/track.rs index fbe67a8fb7c..33ea838f416 100644 --- a/rs/moq-net/src/ietf/track.rs +++ b/rs/moq-net/src/ietf/track.rs @@ -21,7 +21,7 @@ use super::Version; #[derive(Clone, Debug)] pub struct TrackStatus<'a> { pub request_id: RequestId, - pub track_namespace: Path<'a>, + pub track_namespace: Path, pub track_name: Cow<'a, str>, /// Whether the requester wants Track Properties on the answer (INCLUDE_PROPERTIES, /// draft-22). @@ -265,7 +265,7 @@ mod tests { fn test_track_status_v14_round_trip() { let msg = TrackStatus { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), properties_wanted: true, epoch: None, @@ -283,7 +283,7 @@ mod tests { fn test_track_status_v15_round_trip() { let msg = TrackStatus { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), properties_wanted: true, epoch: None, @@ -301,7 +301,7 @@ mod tests { fn test_track_status_v17_round_trip() { let msg = TrackStatus { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), properties_wanted: true, epoch: None, @@ -319,7 +319,7 @@ mod tests { fn test_track_status_v16_round_trip() { let msg = TrackStatus { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), properties_wanted: true, epoch: None, @@ -337,7 +337,7 @@ mod tests { fn test_track_status_v18_round_trip() { let msg = TrackStatus { request_id: RequestId(1), - track_namespace: Path::new("test/ns"), + track_namespace: Path::from_static("test/ns"), track_name: "video".into(), properties_wanted: true, epoch: None, @@ -368,7 +368,7 @@ mod tests { for wanted in [true, false] { let msg = TrackStatus { request_id: RequestId(1), - track_namespace: Path::new("ns"), + track_namespace: Path::from_static("ns"), track_name: "video".into(), properties_wanted: wanted, epoch: None, diff --git a/rs/moq-net/src/lib.rs b/rs/moq-net/src/lib.rs index 9cb5a4c8f1b..4c0b6951a00 100644 --- a/rs/moq-net/src/lib.rs +++ b/rs/moq-net/src/lib.rs @@ -140,7 +140,7 @@ pub use driver::Driver; pub use epoch::{Epoch, InvalidEpoch}; pub use error::*; pub use model::*; -pub use path::{AsPath, InvalidPattern, Path, PathOwned, Pattern, Patterns}; +pub use path::{InvalidPattern, Path, Pattern, Patterns}; #[cfg(feature = "async")] pub use server::Server; #[cfg(feature = "async")] diff --git a/rs/moq-net/src/lite/announce.rs b/rs/moq-net/src/lite/announce.rs index 9a31ad22a85..d210d77f823 100644 --- a/rs/moq-net/src/lite/announce.rs +++ b/rs/moq-net/src/lite/announce.rs @@ -44,19 +44,19 @@ pub fn update_supported(version: Version) -> bool { /// base announcement, resolved against the stream's history by /// [`AnnounceDecoder`](super::AnnounceDecoder). #[derive(Clone, Debug, PartialEq, Eq)] -pub enum AnnounceBroadcast<'a> { +pub enum AnnounceBroadcast { /// ANNOUNCE_START (lite-06) / active (older): a broadcast is now available. /// Carries the path suffix, the hop chain, and (lite-06+) the static route /// cost, and assigns the next announce id. The epoch (lite-07+) changes only /// with a `Restart`. Active { - suffix: PathRef<'a>, + suffix: PathRef, epoch: Option, hops: HopsRef, cost: Cost, }, /// Pre-lite-06: a broadcast is no longer available, retracted by path. - Ended { suffix: Path<'a>, hops: Hops }, + Ended { suffix: Path, hops: Hops }, /// ANNOUNCE_END (lite-06+): a broadcast is no longer available, retracted by /// announce id. The id is retired; referencing it again is a protocol violation. EndedId { id: u64 }, @@ -87,20 +87,20 @@ pub enum AnnounceBroadcast<'a> { /// so 1 is the latest ANNOUNCE_START, and 0 names no base (`keep` must be 0 too). Only /// lite-07 carries a base; every other version is `rest` alone. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct PathRef<'a> { +pub struct PathRef { pub base: u64, pub keep: u64, - pub rest: Path<'a>, + pub rest: Path, } -impl<'a> PathRef<'a> { +impl PathRef { /// The whole suffix, with no base. - pub fn literal(rest: Path<'a>) -> Self { + pub fn literal(rest: Path) -> Self { Self { base: 0, keep: 0, rest } } } -impl Encode for PathRef<'_> { +impl Encode for PathRef { fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { if version.has_announce_compression() { w.varint(self.base)?; @@ -112,7 +112,7 @@ impl Encode for PathRef<'_> { } } -impl Decode for PathRef<'_> { +impl Decode for PathRef { fn decode(buf: &mut Decoder<'_>, version: Version) -> Result { if !version.has_announce_compression() { return Ok(Self::literal(Path::decode(buf, version)?)); @@ -179,39 +179,6 @@ impl Decode for HopsRef { } } -impl AnnounceBroadcast<'_> { - /// Re-own a decoded message so it can outlive the decode buffer. - #[cfg(test)] - pub fn into_owned(self) -> AnnounceBroadcast<'static> { - match self { - Self::Active { - suffix, - epoch, - hops, - cost, - } => AnnounceBroadcast::Active { - suffix: PathRef { - base: suffix.base, - keep: suffix.keep, - rest: suffix.rest.into_owned(), - }, - epoch, - hops, - cost, - }, - Self::Ended { suffix, hops } => AnnounceBroadcast::Ended { - suffix: suffix.into_owned(), - hops, - }, - Self::EndedId { id } => AnnounceBroadcast::EndedId { id }, - Self::Update { id, hops, cost } => AnnounceBroadcast::Update { id, hops, cost }, - Self::Restart { id, epoch, hops, cost } => AnnounceBroadcast::Restart { id, epoch, hops, cost }, - Self::Skipped => AnnounceBroadcast::Skipped, - Self::Synced => AnnounceBroadcast::Synced, - } - } -} - impl Encode for Cost { fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { if !version.has_route_cost() { @@ -242,7 +209,7 @@ impl Decode for Cost { } } -impl Encode for AnnounceBroadcast<'_> { +impl Encode for AnnounceBroadcast { fn encode(&self, w: &mut Encoder<'_>, version: Version) -> Result<(), EncodeError> { w.control = Some(crate::stats::Message::Announce); if version.has_announce_id() { @@ -331,7 +298,7 @@ impl Encode for AnnounceBroadcast<'_> { } } -impl Decode for AnnounceBroadcast<'_> { +impl Decode for AnnounceBroadcast { fn decode(buf: &mut Decoder<'_>, version: Version) -> Result { buf.control = Some(crate::stats::Message::Announce); if version.has_announce_id() { @@ -386,7 +353,7 @@ impl Decode for AnnounceBroadcast<'_> { } } -impl AnnounceBroadcast<'_> { +impl AnnounceBroadcast { /// Decode the body of a pre-lite-06 ANNOUNCE_BROADCAST (inner status + path + hops). fn decode_legacy(r: &mut Decoder<'_>, version: Version) -> Result { let status = AnnounceStatus::decode(r, version)?; @@ -445,9 +412,9 @@ fn encode_hops(w: &mut Encoder<'_>, version: Version, hops: &Hops) -> Result<(), /// ANNOUNCE_REQUEST: sent by the subscriber to request ANNOUNCE_BROADCAST messages /// for a path prefix. Renamed from ANNOUNCE_INTEREST in lite-05. #[derive(Clone, Debug)] -pub struct AnnounceRequest<'a> { +pub struct AnnounceRequest { // Request tracks with this prefix. - pub prefix: Path<'a>, + pub prefix: Path, // Lite04/05 only: unless unknown, the publisher SHOULD skip announces whose hop IDs // contain this value. Not on the wire elsewhere, so the value set here is ignored // when encoding for another version and decodes as unknown; lite-06 carries the @@ -459,7 +426,7 @@ pub struct AnnounceRequest<'a> { pub hidden: bool, } -impl Message for AnnounceRequest<'_> { +impl Message for AnnounceRequest { const CONTROL: Option = Some(crate::stats::Message::Announce); fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { let prefix = Path::decode(r, version)?; @@ -523,12 +490,12 @@ impl Encode for AnnounceStatus { /// /// Used by Draft01/Draft02 only. Draft03 uses individual Announce messages instead. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct AnnounceInit<'a> { +pub struct AnnounceInit { /// List of currently active broadcasts, encoded as suffixes to be combined with the prefix. - pub suffixes: Vec>, + pub suffixes: Vec, } -impl AnnounceInit<'_> { +impl AnnounceInit { /// Decode one size-prefixed ANNOUNCE_INIT, refusing one that lists more than `budget` /// suffixes before decoding any, so the set grows no larger than what may be announced. pub(crate) fn decode_within(r: &mut Decoder<'_>, version: Version, budget: usize) -> Result { @@ -561,7 +528,7 @@ impl AnnounceInit<'_> { } } -impl Message for AnnounceInit<'_> { +impl Message for AnnounceInit { const CONTROL: Option = Some(crate::stats::Message::Announce); // Published lite01/02 carry the whole initial set in one message. const MAX_SIZE: usize = 64 * 1024 * 1024; @@ -637,7 +604,7 @@ mod tests { let version = Version::Lite02; let mut wire = Vec::new(); AnnounceInit { - suffixes: vec![Path::new("a"), Path::new(""), Path::new("b")], + suffixes: vec![Path::from_static("a"), Path::from_static(""), Path::from_static("b")], } .encode(&mut Encoder::new(&mut wire, version.into()), version) .unwrap(); @@ -694,7 +661,7 @@ mod tests { let mut buf = Vec::new(); AnnounceBroadcast::Active { epoch: None, - suffix: PathRef::literal(Path::new("foo/bar")), + suffix: PathRef::literal(Path::from_static("foo/bar")), hops: HopsRef::default(), cost: Cost::default(), } @@ -769,14 +736,14 @@ mod tests { assert_eq!(round_trip(&msg), msg); } - fn broadcast_round_trip(msg: &AnnounceBroadcast, version: Version) -> AnnounceBroadcast<'static> { + fn broadcast_round_trip(msg: &AnnounceBroadcast, version: Version) -> AnnounceBroadcast { let mut buf = Vec::new(); msg.encode(&mut Encoder::new(&mut buf, version.into()), version) .unwrap(); let mut slice = &buf[..]; let got = crate::coding::decode_buf(&mut slice, version, AnnounceBroadcast::decode).unwrap(); assert!(slice.is_empty(), "trailing bytes after decode"); - got.into_owned() + got } #[test] @@ -785,14 +752,14 @@ mod tests { hops.push(Hop::new(7).unwrap()).unwrap(); let msg = AnnounceBroadcast::Active { epoch: None, - suffix: PathRef::literal(Path::new("room/cam")), + suffix: PathRef::literal(Path::from_static("room/cam")), hops: HopsRef::literal(hops.clone()), cost: Cost::UNKNOWN, }; assert_eq!(broadcast_round_trip(&msg, Version::Lite05), msg); let ended = AnnounceBroadcast::Ended { - suffix: Path::new("room/cam"), + suffix: Path::from_static("room/cam"), hops: Hops::new(), }; assert_eq!(broadcast_round_trip(&ended, Version::Lite05), ended); @@ -808,7 +775,7 @@ mod tests { let active = AnnounceBroadcast::Active { epoch: None, - suffix: PathRef::literal(Path::new("room/cam")), + suffix: PathRef::literal(Path::from_static("room/cam")), hops: HopsRef::literal(hops.clone()), cost, }; @@ -837,7 +804,7 @@ mod tests { suffix: PathRef { base: 2, keep: 3, - rest: Path::new("cam"), + rest: Path::from_static("cam"), }, hops: HopsRef { base: 1, @@ -929,7 +896,7 @@ mod tests { suffix: PathRef { base: 1, keep: 1, - rest: Path::new("cam"), + rest: Path::from_static("cam"), }, hops: HopsRef::default(), cost: Cost::default(), @@ -963,7 +930,7 @@ mod tests { )); assert!(matches!( AnnounceBroadcast::Ended { - suffix: Path::new("room/cam"), + suffix: Path::from_static("room/cam"), hops: Hops::new() } .encode(&mut Encoder::new(&mut buf, Version::Lite06.into()), Version::Lite06), @@ -979,7 +946,7 @@ mod tests { fn route_cost_is_dropped_before_lite06() { let msg = AnnounceBroadcast::Active { epoch: None, - suffix: PathRef::literal(Path::new("room/cam")), + suffix: PathRef::literal(Path::from_static("room/cam")), hops: HopsRef::default(), cost: Cost::new(9), }; @@ -988,7 +955,7 @@ mod tests { got, AnnounceBroadcast::Active { epoch: None, - suffix: PathRef::literal(Path::new("room/cam")), + suffix: PathRef::literal(Path::from_static("room/cam")), hops: HopsRef::default(), cost: Cost::UNKNOWN, } @@ -1011,7 +978,7 @@ mod tests { #[test] fn unknown_announce_type_is_skipped() { let mut body = Vec::new(); - Path::new("room/cam") + Path::from_static("room/cam") .encode(&mut Encoder::new(&mut body, Version::Lite06.into()), Version::Lite06) .unwrap(); Hops::new() @@ -1045,7 +1012,7 @@ mod tests { assert_eq!(buf.len(), 3); } - fn request_round_trip(msg: &AnnounceRequest, version: Version) -> AnnounceRequest<'static> { + fn request_round_trip(msg: &AnnounceRequest, version: Version) -> AnnounceRequest { let mut buf = Vec::new(); msg.encode(&mut Encoder::new(&mut buf, version.into()), version) .unwrap(); @@ -1053,7 +1020,7 @@ mod tests { let got = crate::coding::decode_buf(&mut slice, version, AnnounceRequest::decode).unwrap(); assert!(slice.is_empty(), "trailing bytes after decode"); AnnounceRequest { - prefix: got.prefix.to_owned(), + prefix: got.prefix.clone(), exclude_hop: got.exclude_hop, hidden: got.hidden, } @@ -1064,7 +1031,7 @@ mod tests { fn announce_request_carries_hidden_from_lite07() { for hidden in [false, true] { let msg = AnnounceRequest { - prefix: Path::new("room/"), + prefix: Path::from_static("room/"), exclude_hop: Hop::UNKNOWN, hidden, }; @@ -1078,7 +1045,7 @@ mod tests { fn announce_request_rejects_a_bad_hidden_flag() { let mut buf = Vec::new(); let mut body = Vec::new(); - Path::new("room") + Path::from_static("room") .encode(&mut Encoder::new(&mut body, Version::Lite07.into()), Version::Lite07) .unwrap(); body.push(2); @@ -1094,7 +1061,7 @@ mod tests { #[test] fn announce_request_carries_exclude_hop_on_lite05() { let msg = AnnounceRequest { - prefix: Path::new("room/"), + prefix: Path::from_static("room/"), exclude_hop: Hop::new(42).unwrap(), hidden: false, }; @@ -1106,7 +1073,7 @@ mod tests { #[test] fn announce_request_drops_exclude_hop_on_lite06() { let msg = AnnounceRequest { - prefix: Path::new("room/"), + prefix: Path::from_static("room/"), exclude_hop: Hop::new(42).unwrap(), hidden: false, }; diff --git a/rs/moq-net/src/lite/compress.rs b/rs/moq-net/src/lite/compress.rs index 282a898da88..963fce0a0f8 100644 --- a/rs/moq-net/src/lite/compress.rs +++ b/rs/moq-net/src/lite/compress.rs @@ -14,7 +14,7 @@ use std::{ }; use crate::{ - Error, Hop, Hops, Path, PathOwned, + Error, Hop, Hops, Path, coding::{Form, varint}, }; @@ -24,7 +24,7 @@ use super::{HopsRef, PathRef, Version}; /// (the one excluding ANNOUNCE_OK's Hop ID). #[derive(Clone, Debug)] struct Entry { - suffix: PathOwned, + suffix: Path, hops: Hops, } @@ -45,13 +45,18 @@ impl Live { self.entries.get(&id).map(Some).ok_or(Error::ProtocolViolation) } - fn resolve_path(&self, path: PathRef<'_>) -> Result { + fn resolve_path(&self, path: PathRef) -> Result { let Some(base) = self.base(path.base)? else { - return Ok(path.rest.into_owned()); + return Ok(path.rest); }; let keep = usize::try_from(path.keep).map_err(|_| Error::ProtocolViolation)?; let head = head(&base.suffix, keep).ok_or(Error::ProtocolViolation)?; - let suffix = head.join(&path.rest); + let suffix = match (head.is_empty(), path.rest.is_empty()) { + (true, _) => Ok(path.rest), + (false, true) => Path::new(head), + (false, false) => Path::concat(head, path.rest.as_str()), + } + .map_err(|_| Error::ProtocolViolation)?; if suffix.parts().count() > Path::MAX_PARTS { return Err(Error::ProtocolViolation); } @@ -74,16 +79,16 @@ impl Live { } /// The first `keep` segments of `path`, or `None` if it has fewer. -fn head<'a>(path: &'a Path<'_>, keep: usize) -> Option> { +fn head(path: &Path, keep: usize) -> Option<&str> { if keep == 0 { - return Some(Path::empty()); + return Some(""); } let s = path.as_str(); let mut ends = s .match_indices('/') .map(|(i, _)| i) .chain((!s.is_empty()).then_some(s.len())); - ends.nth(keep - 1).map(|end| Path::new(&s[..end])) + ends.nth(keep - 1).map(|end| &s[..end]) } /// Resolves the bases on a received announce stream. Only used on versions with @@ -95,7 +100,7 @@ pub(crate) struct AnnounceDecoder { impl AnnounceDecoder { /// An ANNOUNCE_START: resolve it and assign it the next id. - pub fn start(&mut self, suffix: PathRef<'_>, hops: HopsRef) -> Result<(PathOwned, Hops), Error> { + pub fn start(&mut self, suffix: PathRef, hops: HopsRef) -> Result<(Path, Hops), Error> { let suffix = self.live.resolve_path(suffix)?; let hops = self.live.resolve_hops(hops)?; let id = self.live.next; @@ -112,7 +117,7 @@ impl AnnounceDecoder { /// An ANNOUNCE_UPDATE: resolve its chain before replacing the old one, which it /// may itself be based on. - pub fn update(&mut self, id: u64, hops: HopsRef) -> Result<(PathOwned, Hops), Error> { + pub fn update(&mut self, id: u64, hops: HopsRef) -> Result<(Path, Hops), Error> { let hops = self.live.resolve_hops(hops)?; let entry = self.live.entries.get_mut(&id).ok_or(Error::ProtocolViolation)?; entry.hops = hops.clone(); @@ -120,7 +125,7 @@ impl AnnounceDecoder { } /// An ANNOUNCE_END: retire the id, returning its suffix. - pub fn end(&mut self, id: u64) -> Result { + pub fn end(&mut self, id: u64) -> Result { let entry = self.live.entries.remove(&id).ok_or(Error::ProtocolViolation)?; Ok(entry.suffix) } @@ -245,7 +250,7 @@ impl AnnounceEncoder { } /// An ANNOUNCE_START: its id (on versions that assign one) and its wire form. - pub fn start(&mut self, suffix: PathOwned, hops: Hops) -> (Option, PathRef<'static>, HopsRef) { + pub fn start(&mut self, suffix: Path, hops: Hops) -> (Option, PathRef, HopsRef) { if !self.version.has_announce_id() { return (None, PathRef::literal(suffix), HopsRef::literal(hops)); } @@ -291,7 +296,7 @@ impl AnnounceEncoder { } /// The wire form of `suffix`, sized from its parts so only the winner is built. - fn path_ref(&self, suffix: &PathOwned, next: u64) -> PathRef<'static> { + fn path_ref(&self, suffix: &Path, next: u64) -> PathRef { let form: Form = self.version.into(); let literal = || PathRef::literal(suffix.clone()); let Some((keep, id)) = self.heads.longest(suffix.parts()) else { @@ -300,7 +305,7 @@ impl AnnounceEncoder { // Where the segments after `keep` start, sharing the suffix's buffer. let s = suffix.as_str(); let start = s.match_indices('/').nth(keep - 1).map_or(s.len(), |(i, _)| i + 1); - let rest = suffix.slice_from(start).to_owned(); + let rest = suffix.slice_from(start); let base = next - id; let keep = keep as u64; @@ -347,7 +352,7 @@ fn varint_size(value: u64, form: Form) -> usize { } /// The bytes `path` takes on the wire: a varint length, then the string. -fn string_size(path: &Path<'_>, form: Form) -> usize { +fn string_size(path: &Path, form: Form) -> usize { let len = path.as_str().len(); varint_size(len as u64, form) + len } @@ -363,11 +368,11 @@ mod tests { Hops::try_from(ids.iter().map(|id| Hop::from_wire(*id)).collect::>()).unwrap() } - fn path(base: u64, keep: u64, rest: &str) -> PathRef<'static> { + fn path(base: u64, keep: u64, rest: &str) -> PathRef { PathRef { base, keep, - rest: Path::new(rest).to_owned(), + rest: Path::new(rest).unwrap(), } } @@ -379,7 +384,7 @@ mod tests { } } - fn literal(suffix: &str, ids: &[u64]) -> (PathRef<'static>, HopsRef) { + fn literal(suffix: &str, ids: &[u64]) -> (PathRef, HopsRef) { (path(0, 0, suffix), chain(0, ids, 0)) } @@ -508,10 +513,26 @@ mod tests { )); } + #[test] + fn rejects_a_resolved_path_too_long() { + let mut decoder = AnnounceDecoder::default(); + let head = "x".repeat(Path::MAX_BYTES - 2); + let (p, h) = literal(&head, &[]); + decoder.start(p, h).unwrap(); + + // `head/y` is exactly the limit, and one byte more is refused. + let (suffix, _) = decoder.start(path(1, 1, "y"), chain(0, &[], 0)).unwrap(); + assert_eq!(suffix.len(), Path::MAX_BYTES); + assert!(matches!( + decoder.start(path(1, 1, "yz"), chain(0, &[], 0)), + Err(Error::ProtocolViolation) + )); + } + /// Encode a message with `encoder`, decode it with `decoder`, and check that the /// resolved values match the input. fn start(encoder: &mut AnnounceEncoder, decoder: &mut AnnounceDecoder, suffix: &str, ids: &[u64]) -> (u64, usize) { - let (id, wire_path, wire_hops) = encoder.start(Path::new(suffix).to_owned(), hops(ids)); + let (id, wire_path, wire_hops) = encoder.start(Path::new(suffix).unwrap(), hops(ids)); let size = wire_path.encode_bytes(VERSION).unwrap().len() + wire_hops.encode_bytes(VERSION).unwrap().len(); let (got_path, got_hops) = decoder.start(wire_path, wire_hops).unwrap(); assert_eq!(got_path.as_str(), suffix); @@ -565,7 +586,7 @@ mod tests { // Shares a byte prefix with `a/b/d`, but no second segment. start(&mut encoder, &mut decoder, "a/b-z", &[3]); - let (_, wire_path, wire_hops) = encoder.start(Path::new("a/b/d").to_owned(), hops(&[5, 1u64 << 51])); + let (_, wire_path, wire_hops) = encoder.start(Path::from_static("a/b/d"), hops(&[5, 1u64 << 51])); assert_eq!(wire_path, path(2, 2, "d")); assert_eq!(wire_hops, chain(2, &[5], 1)); } @@ -594,7 +615,7 @@ mod tests { let suffix = format!("f{n}"); start(&mut encoder, &mut decoder, &suffix, &[1]); } - let (_, wire_path, wire_hops) = encoder.start(Path::new("again").to_owned(), hops(&[100])); + let (_, wire_path, wire_hops) = encoder.start(Path::from_static("again"), hops(&[100])); assert_eq!(wire_hops, HopsRef::literal(hops(&[100]))); let (got_path, got_hops) = decoder.start(wire_path, wire_hops).unwrap(); assert_eq!(got_path.as_str(), "again"); @@ -606,19 +627,19 @@ mod tests { let mut encoder = AnnounceEncoder::new(VERSION); let mut decoder = AnnounceDecoder::default(); start(&mut encoder, &mut decoder, "a", &[1]); - let (_, wire_path, wire_hops) = encoder.start(Path::new("b").to_owned(), hops(&[2])); - assert_eq!(wire_path, PathRef::literal(Path::new("b"))); + let (_, wire_path, wire_hops) = encoder.start(Path::from_static("b"), hops(&[2])); + assert_eq!(wire_path, PathRef::literal(Path::from_static("b"))); assert_eq!(wire_hops, HopsRef::literal(hops(&[2]))); } #[test] fn lite06_assigns_ids_without_bases() { let mut encoder = AnnounceEncoder::new(Version::Lite06); - let (id, _, _) = encoder.start(Path::new("a/b").to_owned(), hops(&[1])); + let (id, _, _) = encoder.start(Path::from_static("a/b"), hops(&[1])); assert_eq!(id, Some(0)); - let (id, wire_path, wire_hops) = encoder.start(Path::new("a/c").to_owned(), hops(&[1])); + let (id, wire_path, wire_hops) = encoder.start(Path::from_static("a/c"), hops(&[1])); assert_eq!(id, Some(1)); - assert_eq!(wire_path, PathRef::literal(Path::new("a/c"))); + assert_eq!(wire_path, PathRef::literal(Path::from_static("a/c"))); assert_eq!(wire_hops, HopsRef::literal(hops(&[1]))); encoder.end(0); assert_eq!(encoder.update(1, hops(&[1])), HopsRef::literal(hops(&[1]))); @@ -632,11 +653,11 @@ mod tests { fn golden() -> Vec { use crate::fuzz::Announced; vec![ - Announced::Start(Path::new("room/a/cam").to_owned(), hops(&[0x1111, 0x2222])), - Announced::Start(Path::new("room/a/mic").to_owned(), hops(&[0x3333, 0x2222])), + Announced::Start(Path::from_static("room/a/cam"), hops(&[0x1111, 0x2222])), + Announced::Start(Path::from_static("room/a/mic"), hops(&[0x3333, 0x2222])), Announced::Update(0, hops(&[0x4444, 0x2222])), Announced::End(1), - Announced::Start(Path::new("room/b").to_owned(), hops(&[0x5555, 0x2222])), + Announced::Start(Path::from_static("room/b"), hops(&[0x5555, 0x2222])), ] } diff --git a/rs/moq-net/src/lite/fetch.rs b/rs/moq-net/src/lite/fetch.rs index 4d63673dfbc..b6eaeace88d 100644 --- a/rs/moq-net/src/lite/fetch.rs +++ b/rs/moq-net/src/lite/fetch.rs @@ -12,7 +12,7 @@ use super::{Message, Version}; /// Lite03+ only. #[derive(Clone, Debug)] pub struct Fetch<'a> { - pub broadcast: Path<'a>, + pub broadcast: Path, /// The publisher instance the subscriber expects; see [`crate::origin::Route::epoch`]. /// Lite07+ only. pub epoch: Option, @@ -112,7 +112,7 @@ mod test { fn fetch_sample() -> Fetch<'static> { Fetch { epoch: None, - broadcast: Path::new("room").to_owned(), + broadcast: Path::from_static("room"), track: Cow::Borrowed("video"), priority: 3, max_delay: None, @@ -157,7 +157,7 @@ mod test { fn fetch_roundtrips() { for version in [Version::Lite03, Version::Lite04, Version::Lite05] { let got = fetch_roundtrip(version, &fetch_sample()); - assert_eq!(got.broadcast, Path::new("room")); + assert_eq!(got.broadcast, Path::from_static("room")); assert_eq!(got.track, "video"); assert_eq!(got.priority, 3); assert_eq!(got.group, 7); diff --git a/rs/moq-net/src/lite/publisher.rs b/rs/moq-net/src/lite/publisher.rs index fa356ded43e..a17c502cfda 100644 --- a/rs/moq-net/src/lite/publisher.rs +++ b/rs/moq-net/src/lite/publisher.rs @@ -131,7 +131,7 @@ impl Shared { /// Watches whether the session still lets us serve `broadcast` to the peer: our /// grant, and the ceiling on what the peer may subscribe to. fn gate(&self, broadcast: &crate::Path) -> crate::auth::Gate { - crate::auth::Gate::new(self.auth.clone(), broadcast.to_owned(), crate::auth::Direction::Publish) + crate::auth::Gate::new(self.auth.clone(), broadcast.clone(), crate::auth::Direction::Publish) } /// The origin to resolve a peer-requested broadcast from: excludes routes @@ -274,10 +274,9 @@ impl Publisher { version: Version, auth: Option, ) -> Result<(), Error> { - let mut run = AnnounceRun::new(crate::PathOwned::default(), self_origin, version); - run.auth = auth.map(|handle| { - crate::auth::Gate::new(handle, crate::Path::empty().to_owned(), crate::auth::Direction::Publish) - }); + let mut run = AnnounceRun::new(crate::Path::default(), self_origin, version); + run.auth = + auth.map(|handle| crate::auth::Gate::new(handle, crate::Path::empty(), crate::auth::Direction::Publish)); kio::wait(|waiter| run.poll(stream, origin, announced, waiter)).await } } @@ -627,7 +626,7 @@ impl Enforce { return Poll::Ready(Ok(())); }; tracing::error!( - broadcast = %shared.origin.absolute(&path), + broadcast = %crate::path::Rooted(shared.origin.root(), &path), "publishing outside our grant; closing the session" ); let err = Error::Unauthorized; @@ -852,7 +851,7 @@ enum AnnounceState { Decode, /// Waiting on the peer's SETUP for the session-wide excluded origin /// (lite-05, whose wire carries no per-stream exclude_hop). - ExcludeHop { prefix: crate::PathOwned, hidden: bool }, + ExcludeHop { prefix: crate::Path, hidden: bool }, /// Streaming announce updates. Run { origin: origin::Consumer, @@ -878,7 +877,7 @@ impl AnnounceServe { let stream = self.stream.as_mut().expect("stream present"); let mut cx = waiter.context(); let interest = ready!(stream.reader.poll_decode::(&mut cx))?; - let prefix = interest.prefix.to_owned(); + let prefix = interest.prefix.clone(); let hidden = interest.hidden; // The identity whose routes we filter out. Lite-04/05 carry it per @@ -917,10 +916,10 @@ impl AnnounceServe { | Error::Session(crate::SessionError::Cancel) | Error::Transport(_) | Error::IdleTimeout => { - tracing::debug!(prefix = %origin.absolute(""), "announcing cancelled"); + tracing::debug!(prefix = %origin.root(), "announcing cancelled"); } err => { - tracing::warn!(%err, prefix = %origin.absolute(""), "announcing error"); + tracing::warn!(%err, prefix = %origin.root(), "announcing error"); } } self.stream.take().expect("stream present").writer.abort(&err); @@ -931,7 +930,7 @@ impl AnnounceServe { } } - fn start(&mut self, prefix: crate::PathOwned, exclude_hop: Hop, hidden: bool) { + fn start(&mut self, prefix: crate::Path, exclude_hop: Hop, hidden: bool) { // If the requested prefix is outside our scope (an empty origin, or a token // that doesn't grant it), we simply have nothing to announce. Respond with an // empty set and keep the stream open (the subscriber treats a FIN here as a @@ -981,7 +980,7 @@ impl AnnounceServe { struct AnnounceRun { // The requested prefix. Updates arrive named relative to it, but the grant // matches paths relative to the origin's root. - prefix: crate::PathOwned, + prefix: crate::Path, self_origin: Hop, version: Version, // Lite06+: announce ids. Every `active` we send implicitly assigns the next @@ -991,7 +990,7 @@ struct AnnounceRun { encoder: lite::AnnounceEncoder, // The routes the peer currently holds, keyed by the suffix under the requested // prefix. - live: HashMap, + live: HashMap, phase: AnnouncePhase, initial: Option, replay: bool, @@ -1023,7 +1022,7 @@ enum AnnouncePhase { } impl AnnounceRun { - fn new(prefix: crate::PathOwned, self_origin: Hop, version: Version) -> Self { + fn new(prefix: crate::Path, self_origin: Hop, version: Version) -> Self { Self { prefix, self_origin, @@ -1052,7 +1051,7 @@ impl AnnounceRun { /// Whether we may announce `suffix` (relative to the requested prefix). fn permitted(&self, suffix: &crate::Path) -> bool { - self.permit.matches(self.prefix.join(suffix).as_str()) + self.permit.matches(self.prefix.rejoin(suffix).as_str()) } /// Apply a change to what we may announce: withdraw what it no longer covers and, @@ -1077,7 +1076,7 @@ impl AnnounceRun { .cloned() .collect(); for suffix in revoked { - let absolute = origin.absolute(&suffix); + let absolute = origin.root().rejoin(&suffix); tracing::debug!(route = %absolute, "announce no longer authorized"); self.retract(stream, suffix, &absolute)?; } @@ -1090,7 +1089,7 @@ impl AnnounceRun { // whatever it has pending for the routes the peer holds (an update, a restart, an end // still waiting out its hold) arrives through the loop as usual. let mut snapshot = origin.announced(); - let mut current: HashMap = HashMap::new(); + let mut current: HashMap = HashMap::new(); while let Some(event) = snapshot.try_next() { match event { announce::Event::Start(update) | announce::Event::Update(update) | announce::Event::Restart(update) => { @@ -1106,7 +1105,7 @@ impl AnnounceRun { if self.live.contains_key(&suffix) || !self.permitted(&suffix) { continue; } - let absolute = origin.absolute(&suffix); + let absolute = origin.root().rejoin(&suffix); if let Some((hops, cost)) = self.outgoing(&route, &absolute) { self.advertise(stream, suffix, hops, cost, route.epoch, &absolute)?; } @@ -1119,7 +1118,7 @@ impl AnnounceRun { fn advertise( &mut self, stream: &mut Stream, - suffix: crate::PathOwned, + suffix: crate::Path, hops: Hops, cost: crate::origin::Cost, epoch: Option, @@ -1195,7 +1194,7 @@ impl AnnounceRun { fn start( &mut self, stream: &mut Stream, - suffix: crate::PathOwned, + suffix: crate::Path, hops: Hops, cost: crate::origin::Cost, // Fixed while the peer holds the advertisement: a new one is a restart. @@ -1218,7 +1217,7 @@ impl AnnounceRun { fn restart( &mut self, stream: &mut Stream, - suffix: crate::PathOwned, + suffix: crate::Path, hops: Hops, cost: crate::origin::Cost, epoch: Option, @@ -1254,7 +1253,7 @@ impl AnnounceRun { fn retract( &mut self, stream: &mut Stream, - suffix: crate::PathOwned, + suffix: crate::Path, absolute: &crate::Path, ) -> Result<(), Error> { let Some(advertised) = self.live.remove(&suffix) else { @@ -1302,7 +1301,7 @@ impl AnnounceRun { if !self.permitted(&suffix) { return None; } - let absolute = origin.absolute(&suffix); + let absolute = origin.root().rejoin(&suffix); let (hops, cost) = self.outgoing(&update.route, &absolute)?; tracing::debug!(route = %absolute, "announce"); Some((suffix, hops, cost, update.route.epoch)) @@ -1344,7 +1343,7 @@ impl AnnounceRun { AnnouncePhase::Running => {} } for suffix in self.live.keys().cloned().collect::>() { - self.retract(stream, suffix.clone(), &origin.absolute(&suffix))?; + self.retract(stream, suffix.clone(), &origin.root().rejoin(&suffix))?; } if self.initial.take().is_some() { stream.writer.buffer(&lite::AnnounceBroadcast::Synced)?; @@ -1444,7 +1443,7 @@ impl AnnounceRun { } }; - let absolute = origin.absolute(&update.prefix); + let absolute = origin.root().rejoin(&update.prefix); let suffix = update.prefix; if !active || !self.permitted(&suffix) { @@ -1499,7 +1498,7 @@ trait Request: Sized { type Update: Decode + std::fmt::Debug; /// The broadcast and track the request names. - fn target(msg: &Self::Message) -> (&crate::Path<'static>, &str); + fn target(msg: &Self::Message) -> (&crate::Path, &str); /// The publisher instance the request names, if any. fn epoch(msg: &Self::Message) -> Option<&crate::Epoch>; @@ -1550,8 +1549,9 @@ struct RequestServe { update: Option, /// Ends the request, even mid-reply, once the session stops allowing its broadcast. gate: Option, - // Log context, filled in after the decode. - absolute: crate::PathOwned, + // Log context, filled in after the decode. Relative to the origin's root, so a path + // too long under it is refused by the lookup, on this stream alone. + broadcast: crate::Path, track: String, // A reply that always has another group ready still lets the session's other tasks run. budget: kio::coop::Budget, @@ -1592,7 +1592,7 @@ impl RequestServe { _subscription: None, update: None, gate: None, - absolute: Default::default(), + broadcast: Default::default(), track: Default::default(), budget: kio::coop::Budget::new(32), malformed: false, @@ -1609,7 +1609,7 @@ impl RequestServe { fn poll(&mut self, waiter: &kio::Waiter) -> Poll> { match ready!(self.poll_serve(waiter)) { Ok(()) => { - tracing::debug!(broadcast = %self.absolute, track = %self.track, "{} complete", R::KIND); + tracing::debug!(broadcast = %crate::path::Rooted(self.shared.origin.root(), &self.broadcast), track = %self.track, "{} complete", R::KIND); Poll::Ready(Ok(())) } // The peer's own malformed bytes are a control violation, even after request setup. @@ -1617,13 +1617,13 @@ impl RequestServe { Err(err) => { match &err { err if is_cancel(err) => { - tracing::debug!(broadcast = %self.absolute, track = %self.track, "{} cancelled", R::KIND) + tracing::debug!(broadcast = %crate::path::Rooted(self.shared.origin.root(), &self.broadcast), track = %self.track, "{} cancelled", R::KIND) } Error::Unauthorized | Error::Stream(crate::StreamError::Unauthorized) => { - tracing::info!(broadcast = %self.absolute, track = %self.track, "{} unauthorized", R::KIND) + tracing::info!(broadcast = %crate::path::Rooted(self.shared.origin.root(), &self.broadcast), track = %self.track, "{} unauthorized", R::KIND) } err => { - tracing::warn!(broadcast = %self.absolute, track = %self.track, %err, "{} error", R::KIND) + tracing::warn!(broadcast = %crate::path::Rooted(self.shared.origin.root(), &self.broadcast), track = %self.track, %err, "{} error", R::KIND) } } self.stream.take().expect("stream present").writer.abort(&err); @@ -1659,12 +1659,12 @@ impl RequestServe { RequestState::Decode => { let msg = ready!(stream.reader.poll_decode::(&mut cx))?; let (broadcast, track) = R::target(&msg); - self.absolute = self.shared.origin.absolute(broadcast).to_owned(); + self.broadcast = broadcast.clone(); self.track = track.to_string(); if R::SUBSCRIPTION { self._subscription = Some(self.shared.subscriptions.hold()); } - tracing::debug!(broadcast = %self.absolute, track = %self.track, request = ?msg, "{} started", R::KIND); + tracing::debug!(broadcast = %crate::path::Rooted(self.shared.origin.root(), &self.broadcast), track = %self.track, request = ?msg, "{} started", R::KIND); // Checked before anything is resolved, so a broadcast the session does not // allow is never served, not just cut off later. let mut gate = self.shared.gate(R::target(&msg).0); @@ -1807,7 +1807,7 @@ impl Request for TrackInfoServe { type Message = lite::Track<'static>; type Update = NoUpdate; - fn target(msg: &Self::Message) -> (&crate::Path<'static>, &str) { + fn target(msg: &Self::Message) -> (&crate::Path, &str) { (&msg.broadcast, &msg.track) } @@ -1858,10 +1858,10 @@ impl Request for TracksServe { const KIND: &'static str = "tracks"; const GRACEFUL: bool = false; const HOLDS: bool = false; - type Message = lite::Tracks<'static>; + type Message = lite::Tracks; type Update = NoUpdate; - fn target(msg: &Self::Message) -> (&crate::Path<'static>, &str) { + fn target(msg: &Self::Message) -> (&crate::Path, &str) { (&msg.broadcast, "") } @@ -1955,7 +1955,7 @@ impl Request for SubscribeServe { type Message = lite::Subscribe<'static>; type Update = lite::SubscribeUpdate; - fn target(msg: &Self::Message) -> (&crate::Path<'static>, &str) { + fn target(msg: &Self::Message) -> (&crate::Path, &str) { (&msg.broadcast, &msg.track) } @@ -2195,7 +2195,7 @@ impl Request for FetchServe { type Message = lite::Fetch<'static>; type Update = NoUpdate; - fn target(msg: &Self::Message) -> (&crate::Path<'static>, &str) { + fn target(msg: &Self::Message) -> (&crate::Path, &str) { (&msg.broadcast, &msg.track) } @@ -2618,7 +2618,7 @@ mod announce_test { #[test] fn initial_replay_keeps_the_last_event_and_its_order() { let update = |prefix: &'static str, cost| announce::Announce { - prefix: crate::PathOwned::from(prefix), + prefix: crate::Path::try_from(prefix).unwrap(), captures: None, route: origin::Route::default().with_cost(cost), }; @@ -2638,7 +2638,10 @@ mod announce_test { .collect(); assert_eq!( initial, - vec![("a".into(), origin::Cost::new(3)), ("b".into(), origin::Cost::new(6))] + vec![ + (crate::Path::from_static("a"), origin::Cost::new(3)), + (crate::Path::from_static("b"), origin::Cost::new(6)) + ] ); } @@ -2663,7 +2666,7 @@ mod announce_test { ), reader: Reader::new(crate::transport::boxed::RecvStream::new(PendingRecv), Version::Lite07), }; - let mut run = AnnounceRun::new(crate::Path::new("").to_owned(), consumer.hop(), Version::Lite07); + let mut run = AnnounceRun::new(crate::Path::from_static(""), consumer.hop(), Version::Lite07); let waiter = kio::Waiter::noop(); assert!(run.poll(&mut stream, &consumer, &mut announced, &waiter).is_pending()); drop(removed); @@ -2705,7 +2708,7 @@ mod announce_test { ), reader: Reader::new(crate::transport::boxed::RecvStream::new(PendingRecv), Version::Lite07), }; - let mut run = AnnounceRun::new(crate::Path::new("").to_owned(), consumer.hop(), Version::Lite07); + let mut run = AnnounceRun::new(crate::Path::from_static(""), consumer.hop(), Version::Lite07); let waiter = kio::Waiter::noop(); assert!(run.poll(&mut stream, &consumer, &mut announced, &waiter).is_pending()); let auth = crate::auth::Link::new(false); @@ -2747,7 +2750,7 @@ mod announce_test { ), reader: Reader::new(crate::transport::boxed::RecvStream::new(PendingRecv), Version::Lite07), }; - let mut run = AnnounceRun::new(crate::Path::new("").to_owned(), consumer.hop(), Version::Lite07); + let mut run = AnnounceRun::new(crate::Path::from_static(""), consumer.hop(), Version::Lite07); let waiter = kio::Waiter::noop(); assert!(run.poll(&mut stream, &consumer, &mut announced, &waiter).is_pending()); let auth = crate::auth::Link::new(false); @@ -2803,7 +2806,7 @@ mod announce_test { ), reader: Reader::new(crate::transport::boxed::RecvStream::new(PendingRecv), Version::Lite07), }; - let mut run = AnnounceRun::new(crate::Path::new("").to_owned(), consumer.hop(), Version::Lite07); + let mut run = AnnounceRun::new(crate::Path::from_static(""), consumer.hop(), Version::Lite07); let waiter = kio::Waiter::noop(); assert!(run.poll(&mut stream, &consumer, &mut announced, &waiter).is_pending()); drop(unseen); @@ -2843,7 +2846,7 @@ mod announce_test { ), reader: Reader::new(crate::transport::boxed::RecvStream::new(PendingRecv), Version::Lite07), }; - let mut run = AnnounceRun::new(crate::Path::new("").to_owned(), consumer.hop(), Version::Lite07); + let mut run = AnnounceRun::new(crate::Path::from_static(""), consumer.hop(), Version::Lite07); assert!( run.poll(&mut stream, &consumer, &mut announced, &kio::Waiter::noop()) .is_pending() @@ -2881,15 +2884,14 @@ mod announce_test { } /// Decode every announce message written since the last call. - fn take_announces(&mut self) -> Vec> { + fn take_announces(&mut self) -> Vec { let buf = self.pending(); let mut slice = &buf[..]; let mut msgs = Vec::new(); while !slice.is_empty() { msgs.push( crate::coding::decode_buf(&mut slice, self.version, lite::AnnounceBroadcast::decode) - .expect("announce message") - .into_owned(), + .expect("announce message"), ); } self.cursor += buf.len(); @@ -3095,7 +3097,7 @@ mod announce_test { // A restart waits out the update hold like an update. moq_clock::sleep(crate::origin::Config::default().update_hold).await; settle().await; - let restarted = |msgs: Vec>| match (version, msgs.as_slice()) { + let restarted = |msgs: Vec| match (version, msgs.as_slice()) { ( Version::Lite07, [ @@ -7317,7 +7319,7 @@ mod tests { let consumer = origin.consume(); let mut announced = consumer.announced(); - let mut run = AnnounceRun::new(crate::PathOwned::default(), self_origin, Version::Lite01); + let mut run = AnnounceRun::new(crate::Path::default(), self_origin, Version::Lite01); run.withdraw(&mut stream, &consumer, &mut announced).unwrap(); let _ = kio::wait(|waiter| Poll::Ready(run.poll(&mut stream, &consumer, &mut announced, waiter))).await; @@ -7723,7 +7725,7 @@ mod tests { /// nobody will read. #[moq_clock::test] async fn a_request_ends_when_the_requester_leaves() { - let broadcast = crate::Path::new("room"); + let broadcast = crate::Path::from_static("room"); for version in [Version::Lite05, Version::Lite06, Version::Lite07] { for wait in [Wait::Broadcast, Wait::Track] { for close in [Close::Fin, Close::Reset] { @@ -7762,6 +7764,71 @@ mod tests { } } + /// A broadcast that overflows `Path::MAX_BYTES` under a deep root is refused on its own + /// stream, never by closing the session. + #[moq_clock::test] + async fn a_broadcast_too_long_under_the_root_is_refused_per_request() { + for version in [Version::Lite05, Version::Lite06, Version::Lite07] { + let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); + let root = "r".repeat(crate::Path::MAX_BYTES - 2); + let scoped = origin + .consume() + .scope(root.as_str(), &crate::Patterns::from(crate::Pattern::all())) + .unwrap(); + + let peer_setup = crate::lite::PeerSetup::new(false, crate::transport::Address::Setup); + peer_setup.set(crate::lite::Setup::default()); + let (_, goaway) = crate::goaway::Handle::new(true); + let publisher = Publisher::new(PublisherConfig { + estimate: kio::Producer::default().weak(), + runtime: crate::time::sim(), + session: crate::driver::Transport::new(ScriptedSession::new(Vec::new())), + origin: scoped, + version, + peer_setup, + goaway, + peer_hop: None, + subscriptions: Default::default(), + auth: crate::auth::Link::new(false), + peer_grant: Default::default(), + client: false, + }); + + let mut script = Vec::new(); + lite::Subscribe { + order: crate::track::Order::Descending, + epoch: None, + id: 0, + broadcast: crate::Path::from_static("room"), + track: "video".into(), + priority: 0, + max_delay: Duration::ZERO, + ranges: vec![lite::Range::span(None, 0, None, None)], + } + .encode(&mut crate::coding::Encoder::new(&mut script, version.into()), version) + .unwrap(); + let mut session = ScriptedSession::new(script); + let (send, recv) = futures::future::poll_fn(|cx| { + ::poll_open_bi(&mut session, cx, Default::default()) + }) + .await + .unwrap(); + let stream = Stream:: { + writer: Writer::new(crate::transport::boxed::SendStream::new(send), version), + reader: crate::coding::Reader::new(crate::transport::boxed::RecvStream::new(recv), version), + }; + let mut serve = RequestServe::::new(publisher.shared.clone(), stream); + let served = moq_clock::timeout(Duration::from_millis(1), kio::wait(|waiter| serve.poll(waiter))) + .await + .expect("the request was left waiting"); + assert!(served.is_ok(), "{version:?}: the session closed: {served:?}"); + assert!( + !session.log.resets().is_empty(), + "{version:?}: the request was not refused" + ); + } + } + /// An answered TRACK stream holds its track until the requester closes its side, by /// FIN or reset, and the hold owes nothing a draining close would wait for. A requester /// that leaves while the reply is still blocked on flow control lets the track go too. @@ -7797,7 +7864,7 @@ mod tests { let mut script = Vec::new(); lite::Track { epoch: None, - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), track: "video".into(), } .encode(&mut crate::coding::Encoder::new(&mut script, version.into()), version) @@ -7894,7 +7961,7 @@ mod tests { order: crate::track::Order::Descending, epoch: None, id: 0, - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), track: "video".into(), priority: 1, max_delay: Duration::ZERO, @@ -8005,7 +8072,7 @@ mod tests { order: crate::track::Order::Descending, epoch: None, id: 0, - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), track: "video".into(), priority: 0, max_delay: Duration::from_secs(3600), @@ -8126,7 +8193,7 @@ mod tests { let mut script = Vec::new(); lite::Fetch { epoch: None, - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), track: "video".into(), priority: 0, max_delay: Some(Duration::from_millis(500)), @@ -8250,7 +8317,7 @@ mod tests { order: crate::track::Order::Descending, epoch: None, id: 0, - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), track: "video".into(), priority: 0, max_delay: Duration::ZERO, @@ -8304,7 +8371,7 @@ mod tests { let broadcast = origin.create_broadcast("room").unwrap(); broadcast.announce(Default::default()).unwrap(); let request = lite::Tracks { - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), epoch: None, }; let request = request.encode_bytes(version).unwrap().to_vec(); @@ -8361,7 +8428,7 @@ mod tests { order: crate::track::Order::Descending, epoch: None, id: 0, - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), track: "video".into(), priority: 0, max_delay: Duration::ZERO, @@ -8498,7 +8565,7 @@ mod tests { order: crate::track::Order::Descending, epoch: None, id: 0, - broadcast: crate::Path::new(broadcast), + broadcast: crate::Path::new(broadcast).unwrap(), track: track.into(), priority, max_delay: Duration::ZERO, diff --git a/rs/moq-net/src/lite/subscribe.rs b/rs/moq-net/src/lite/subscribe.rs index c71c78eb889..51e5cc53069 100644 --- a/rs/moq-net/src/lite/subscribe.rs +++ b/rs/moq-net/src/lite/subscribe.rs @@ -16,7 +16,7 @@ use crate::track::Order; #[derive(Clone, Debug)] pub struct Subscribe<'a> { pub id: u64, - pub broadcast: Path<'a>, + pub broadcast: Path, /// The publisher instance the subscriber expects; see [`crate::origin::Route::epoch`]. /// Lite07+ only. pub epoch: Option, @@ -913,7 +913,7 @@ mod test { order: crate::track::Order::Descending, epoch: None, id: 1, - broadcast: Path::new("room").to_owned(), + broadcast: Path::from_static("room"), track: Cow::Borrowed("video"), priority: 3, max_delay: std::time::Duration::from_millis(250), @@ -1046,7 +1046,7 @@ mod test { let mut buf = Vec::new(); let mut w = Encoder::new(&mut buf, version.into()); w.varint(1).unwrap(); - Path::new("room").encode(&mut w, version).unwrap(); + Path::from_static("room").encode(&mut w, version).unwrap(); w.string("video").unwrap(); w.u8(0); w.varint(0).unwrap(); // max delay diff --git a/rs/moq-net/src/lite/subscriber.rs b/rs/moq-net/src/lite/subscriber.rs index 29f1067ebd5..835d46e7102 100644 --- a/rs/moq-net/src/lite/subscriber.rs +++ b/rs/moq-net/src/lite/subscriber.rs @@ -9,7 +9,7 @@ use std::{ }; use crate::{ - AsPath, Error, Path, PathOwned, Timescale, Timestamp, bandwidth, + Error, Path, Timescale, Timestamp, bandwidth, coding::{Reader, Stream, varint::MAX_QUIC}, lite, track::{Anchor, Position, Subscription}, @@ -177,8 +177,8 @@ impl Subscriber { /// bookkeeping in `run`. fn handle_announce( &mut self, - prefix: &PathOwned, - announce: lite::AnnounceBroadcast<'_>, + prefix: &Path, + announce: lite::AnnounceBroadcast, run: &mut PrefixRun, ) -> Result<(), Error> { match announce { @@ -192,9 +192,9 @@ impl Subscriber { // Every `active` assigns the next ordinal, even ones we drop locally. true => run.decoder.start(suffix, hops)?, // Nothing references an announcement here, so there are no bases. - false => (suffix.rest.into_owned(), hops.literal), + false => (suffix.rest, hops.literal), }; - let path = prefix.join(&suffix); + let path = prefix.join(&suffix)?; if lite::update_supported(self.version) && !self.version.has_announce_id() && run.announced.contains(&path) @@ -224,14 +224,14 @@ impl Subscriber { } } lite::AnnounceBroadcast::Ended { suffix, .. } => { - let path = prefix.join(&suffix); + let path = prefix.join(&suffix)?; tracing::debug!(broadcast = %self.log_path(&path), "unannounced"); run.announced.withdraw(&path); } lite::AnnounceBroadcast::EndedId { id } => { // Resolve and retire the id; an unknown or already-retired id is a // protocol violation. - let path = prefix.join(&run.decoder.end(id)?); + let path = prefix.join(&run.decoder.end(id)?)?; tracing::debug!(broadcast = %self.log_path(&path), "unannounced"); run.announced.withdraw(&path); } @@ -239,7 +239,7 @@ impl Subscriber { // Resolve the id; it stays live (the replacement reuses it). An unknown // or retired id is a protocol violation. let (suffix, hops) = run.decoder.update(id, hops)?; - let path = prefix.join(&suffix); + let path = prefix.join(&suffix)?; self.update_announce( path, hops, @@ -252,7 +252,7 @@ impl Subscriber { lite::AnnounceBroadcast::Restart { id, epoch, hops, cost } => { // Resolved like an update: the id stays live. let (suffix, hops) = run.decoder.update(id, hops)?; - let path = prefix.join(&suffix); + let path = prefix.join(&suffix)?; run.announced.restart(&path, epoch); self.restart_announce( path, @@ -276,7 +276,7 @@ impl Subscriber { )] fn start_announce( &mut self, - path: PathOwned, + path: Path, // The publisher instance the peer announced, fixed until it retracts. epoch: Option, hops: crate::Hops, @@ -329,7 +329,7 @@ impl Subscriber { /// since it never changes the content. fn announced_route( &self, - path: &PathOwned, + path: &Path, hops: crate::Hops, cost: crate::origin::Cost, link_cost: u64, @@ -374,7 +374,7 @@ impl Subscriber { /// route is now gone), `Ok(true)` otherwise. fn update_announce( &mut self, - path: PathOwned, + path: Path, hops: crate::Hops, // The route cost off the wire and this link's price. See `start_announce`. cost: crate::origin::Cost, @@ -416,7 +416,7 @@ impl Subscriber { /// route is now gone), `Ok(true)` otherwise. fn restart_announce( &mut self, - path: PathOwned, + path: Path, hops: crate::Hops, // See `update_announce`. cost: crate::origin::Cost, @@ -437,12 +437,7 @@ impl Subscriber { /// The full chain of a new or replacing advertisement, or `None` when it is /// reflected or exceeds the origin's hop limit. - fn chain( - &self, - path: &PathOwned, - mut hops: crate::Hops, - responder_origin: Option, - ) -> Option { + fn chain(&self, path: &Path, mut hops: crate::Hops, responder_origin: Option) -> Option { let reflected = match responder_origin { // A chain already naming the sender came back through it. Zero names // nobody and may repeat; every identified sender must be appended once. @@ -533,8 +528,8 @@ impl Subscriber { Ok(()) } - fn log_path(&self, path: impl AsPath) -> Path<'_> { - self.origin.root().join(path) + fn log_path<'a>(&'a self, path: &'a Path) -> crate::path::Rooted<'a> { + crate::path::Rooted(self.origin.root(), path) } } @@ -589,7 +584,7 @@ pub(super) struct SubscriberDriver { discovery: crate::discovery::Discovery, /// One machine per prefix asked about. Only an error ends the session; a /// prefix finishing cleanly (publisher FIN) just retires. - prefixes: HashMap, + prefixes: HashMap, uni: UniAccept, /// PROBE feedback; finishes quietly when unsupported or given up on. bandwidth: Option, @@ -1566,7 +1561,7 @@ impl ProbeStream { /// session; a publisher FIN is a clean end for the prefix alone. struct AnnouncePrefix { subscriber: Subscriber, - prefix: PathOwned, + prefix: Path, state: PrefixState, /// Holds the origin's announce consumers under the prefix unsynced /// until the peer's initial set is in. Dropped at the boundary the version @@ -1623,7 +1618,7 @@ struct PrefixRun { impl AnnouncePrefix { /// `snapshot` was taken when the prefix was first wanted, so a consumer of the /// origin never reads as synced before the request even goes out. - fn new(subscriber: Subscriber, prefix: PathOwned, snapshot: crate::model::Snapshot) -> Self { + fn new(subscriber: Subscriber, prefix: Path, snapshot: crate::model::Snapshot) -> Self { Self { subscriber, prefix, @@ -1661,7 +1656,7 @@ impl AnnouncePrefix { // the origin, and each local reader opts in on its own // (`origin::Consumer::with_hidden`). stream.writer.buffer(&lite::AnnounceRequest { - prefix: self.prefix.as_path(), + prefix: self.prefix.clone(), exclude_hop: self.subscriber.self_origin, hidden: true, })?; @@ -1746,7 +1741,7 @@ impl AnnouncePrefix { .poll_decode_with(&mut cx, |r| lite::AnnounceInit::decode_within(r, version, budget)) )?; for suffix in msg.suffixes { - let path = self.prefix.join(&suffix); + let path = self.prefix.join(&suffix)?; // Lite01/02 don't carry hop information; the broadcast starts with // an empty chain and an unpriced link. Stats are attributed in the // model when this enters the origin via `create_broadcast`. @@ -1837,7 +1832,7 @@ impl AnnouncePrefix { /// A source the announce half minted for one requested path, for the driver to serve. struct MintedSource { - path: PathOwned, + path: Path, /// The publisher instance the route announced, asked for on every request. epoch: Option, source: crate::model::broadcast::SourceGuard, @@ -1855,7 +1850,7 @@ struct MintedSource { /// dying drops them. struct SourceServe { subscriber: Subscriber, - path: PathOwned, + path: Path, epoch: Option, source: crate::model::broadcast::SourceGuard, dynamic: crate::broadcast::Requests, @@ -1919,7 +1914,7 @@ impl TracksRecv { stream = stream.with_stats(source.subscriber.origin.stats()); stream.writer.buffer(&lite::ControlType::Tracks)?; stream.writer.buffer(&lite::Tracks { - broadcast: source.path.as_path(), + broadcast: source.path.clone(), epoch: source.epoch.clone(), })?; self.stream.insert(stream) @@ -3458,7 +3453,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }; @@ -3520,7 +3515,7 @@ mod tests { validity: selection.validity(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }; @@ -3624,7 +3619,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }; @@ -3694,7 +3689,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }; @@ -3766,7 +3761,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }; @@ -3950,7 +3945,7 @@ mod tests { validity: Default::default(), route: None, subscriber: subscriber.clone(), - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }, @@ -4024,7 +4019,7 @@ mod tests { validity: crate::model::broadcast::RequestValidity::token(Some(route.consume())), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".into(), }; @@ -4097,7 +4092,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }, @@ -4143,7 +4138,7 @@ mod tests { let serve = SourceServe::new( subscriber, MintedSource { - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, source: crate::model::broadcast::SourceGuard::new(broadcast), dynamic, @@ -4295,7 +4290,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".to_string(), }, @@ -4346,7 +4341,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room/host").to_owned(), + path: Path::from_static("room/host"), epoch: None, name: "catalog.json".to_string(), }; @@ -4413,7 +4408,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room/host").to_owned(), + path: Path::from_static("room/host"), epoch: None, name: "audio".to_string(), }; @@ -4482,7 +4477,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room/host").to_owned(), + path: Path::from_static("room/host"), epoch: None, name: "catalog.json".to_string(), }, @@ -4548,7 +4543,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".into(), }; @@ -4603,7 +4598,7 @@ mod tests { validity: Default::default(), route: None, subscriber, - path: Path::new("room").to_owned(), + path: Path::from_static("room"), epoch: None, name: "video".into(), }; @@ -5000,7 +4995,7 @@ mod tests { auth: crate::auth::Link::new(false), }); - let path = Path::new("room/host").to_owned(); + let path = Path::from_static("room/host"); let mut announced = Announced::default(); assert!( subscriber @@ -5060,7 +5055,7 @@ mod tests { assert!( subscriber .start_announce( - Path::new(&format!("room/{i}")).to_owned(), + Path::new(&format!("room/{i}")).unwrap(), None, crate::Hops::new(), crate::origin::Cost::default(), @@ -5108,7 +5103,7 @@ mod tests { going_away: Default::default(), auth: crate::auth::Link::new(false), }); - let path = Path::new("pool").to_owned(); + let path = Path::from_static("pool"); let mut announced = Announced::default(); subscriber .start_announce( @@ -5180,7 +5175,7 @@ mod tests { let mut announced = Announced::default(); subscriber .start_announce( - Path::new("pool").to_owned(), + Path::from_static("pool"), None, crate::Hops::new(), crate::origin::Cost::default(), @@ -5234,7 +5229,7 @@ mod tests { auth: crate::auth::Link::new(false), }); - let path = Path::new("room/host").to_owned(); + let path = Path::from_static("room/host"); let mut announced = Announced::default(); // A chain that already names us is a reflection, declined but still recorded. @@ -5293,7 +5288,7 @@ mod tests { auth: crate::auth::Link::new(false), }); - let path = Path::new("room/host").to_owned(); + let path = Path::from_static("room/host"); let mut announced = Announced::default(); let mut reflected = crate::Hops::new(); reflected.push(assigned).unwrap(); @@ -5359,7 +5354,7 @@ mod tests { let mut announced = Announced::default(); let accepted = subscriber .start_announce( - Path::new("room/host").to_owned(), + Path::from_static("room/host"), None, hops, crate::origin::Cost::default(), @@ -5425,7 +5420,7 @@ mod tests { let mut announced = Announced::default(); let accepted = subscriber .start_announce( - Path::new("room/host").to_owned(), + Path::from_static("room/host"), None, hops, crate::origin::Cost::default(), @@ -5484,7 +5479,7 @@ mod tests { let mut announced = Announced::default(); let accepted = subscriber .start_announce( - Path::new("room/host").to_owned(), + Path::from_static("room/host"), None, crate::Hops::new(), crate::origin::Cost::UNKNOWN, @@ -5531,7 +5526,7 @@ mod tests { assert!( subscriber .start_announce( - Path::new("room/host").to_owned(), + Path::from_static("room/host"), None, hops, crate::origin::Cost::UNKNOWN, @@ -5559,7 +5554,7 @@ mod tests { let mut announced = Announced::default(); subscriber .start_announce( - Path::new("room/host").to_owned(), + Path::from_static("room/host"), None, crate::Hops::new(), crate::origin::Cost::UNKNOWN, @@ -5578,7 +5573,7 @@ mod tests { // A reprice from the same unnamed publisher stays anonymous and in place. subscriber .update_announce( - Path::new("room/host").to_owned(), + Path::from_static("room/host"), crate::Hops::new(), crate::origin::Cost::UNKNOWN, 4, @@ -5617,7 +5612,7 @@ mod tests { let (mut subscriber, consumer) = restart_subscriber(SinkSession::new(Default::default())); let mut announced = Announced::default(); - let path = Path::new("room/host").to_owned(); + let path = Path::from_static("room/host"); subscriber .start_announce( path.clone(), @@ -5658,7 +5653,7 @@ mod tests { let auth = subscriber.auth.clone(); let mut announced = Announced::default(); - let path = Path::new("room/host").to_owned(); + let path = Path::from_static("room/host"); subscriber .start_announce( path.clone(), @@ -5717,7 +5712,7 @@ mod tests { auth: crate::auth::Link::new(false), }); - let path = Path::new("room/host").to_owned(); + let path = Path::from_static("room/host"); let hops = crate::Hops::try_from(vec![crate::Hop::new(7).unwrap()]).unwrap(); let mut announced = Announced::default(); subscriber @@ -5766,7 +5761,7 @@ mod tests { const VERSION: Version = Version::Lite06; let start = |suffix| lite::AnnounceBroadcast::Active { epoch: None, - suffix: lite::PathRef::literal(Path::new(suffix)), + suffix: lite::PathRef::literal(Path::new(suffix).unwrap()), hops: lite::HopsRef::literal(crate::Hops::new()), cost: crate::origin::Cost::default(), }; @@ -5802,7 +5797,7 @@ mod tests { }); let mut prefix = AnnouncePrefix::new( subscriber.clone(), - Path::new("").to_owned(), + Path::from_static(""), subscriber.origin.snapshot(""), ); let mut cursor = consumer.announced(); @@ -5840,7 +5835,7 @@ mod tests { move |w| { lite::AnnounceBroadcast::Active { epoch: None, - suffix: lite::PathRef::literal(Path::new(suffix)), + suffix: lite::PathRef::literal(Path::new(suffix).unwrap()), hops: lite::HopsRef::literal(crate::Hops::new()), cost: crate::origin::Cost::default(), } @@ -5871,7 +5866,7 @@ mod tests { ( AnnouncePrefix::new( subscriber.clone(), - Path::new("").to_owned(), + Path::from_static(""), subscriber.origin.snapshot(""), ), consumer, @@ -6032,7 +6027,7 @@ mod tests { const VERSION: Version = Version::Lite02; let init = |w: &mut crate::coding::Encoder<'_>| { lite::AnnounceInit { - suffixes: vec![Path::new("a")], + suffixes: vec![Path::from_static("a")], } .encode(w, VERSION) .unwrap() @@ -6056,7 +6051,7 @@ mod tests { const VERSION: Version = Version::Lite06; let start = |suffix| lite::AnnounceBroadcast::Active { epoch: None, - suffix: lite::PathRef::literal(Path::new(suffix)), + suffix: lite::PathRef::literal(Path::new(suffix).unwrap()), hops: lite::HopsRef::literal(crate::Hops::new()), cost: crate::origin::Cost::default(), }; @@ -6091,7 +6086,7 @@ mod tests { subscriber.announces = slots.clone(); let mut prefix = AnnouncePrefix::new( subscriber.clone(), - Path::new("").to_owned(), + Path::from_static(""), subscriber.origin.snapshot(""), ); @@ -6283,17 +6278,17 @@ enum Sub { /// A declined advertisement remains present with no route because the peer still /// owns its path and announce id until it retracts or restarts it. struct Announced { - routes: HashMap, + routes: HashMap, /// The session's announce cap, and one slot taken from it per entry in `routes`. slots: crate::session::Slots, held: Vec, /// The epoch each advertisement named, kept while it stands even when declined, /// so a restart that attaches it later serves the same broadcast. - epochs: HashMap, + epochs: HashMap, /// Attached routes whose request queue woke since the last serve pass. The /// driver wakes for every group the session carries, so a pass must cost what /// was requested, not every route the peer announced. - ready: kio::Queue, + ready: kio::Queue, } /// What we made of one advertisement the peer holds. @@ -6325,12 +6320,12 @@ impl Announced { } } - fn contains(&self, path: &PathOwned) -> bool { + fn contains(&self, path: &Path) -> bool { self.routes.contains_key(path) } /// Attach a route, queued for its first serve pass. - fn attach(&mut self, path: PathOwned, route: crate::origin::Route, dynamic: crate::origin::Requests) { + fn attach(&mut self, path: Path, route: crate::origin::Route, dynamic: crate::origin::Requests) { let wake = crate::util::QueueWake::new(path.clone(), self.ready.clone()); let route = AnnouncedRoute::new(route, dynamic, wake); route.waker.wake_by_ref(); @@ -6340,7 +6335,7 @@ impl Announced { /// Put the peer's route into the origin, so paths under it resolve through this /// session on demand, or hold it back when the session's limit does not cover it. /// Returns whether it attached. - fn offer(&mut self, subscriber: &Subscriber, path: PathOwned, route: crate::origin::Route) -> bool { + fn offer(&mut self, subscriber: &Subscriber, path: Path, route: crate::origin::Route) -> bool { if !subscriber .auth .within_limit(crate::auth::Direction::Subscribe, path.as_str()) @@ -6362,7 +6357,7 @@ impl Announced { } } - fn declined(&mut self, path: PathOwned) { + fn declined(&mut self, path: Path) { // Dropping a replaced route closes its sources. self.routes.insert(path, Held::Declined); } @@ -6378,7 +6373,7 @@ impl Announced { /// with [`Self::contains`]. Overwriting an attached route is [`Self::declined`]'s job. /// /// Fails with [`Error::TooManyRequests`] once the session holds as many as it allows. - fn reserve(&mut self, path: PathOwned, epoch: Option) -> Result<(), Error> { + fn reserve(&mut self, path: Path, epoch: Option) -> Result<(), Error> { debug_assert!(!self.routes.contains_key(&path), "reserved a prefix already advertised"); self.held.push(self.slots.acquire()?); if let Some(epoch) = epoch { @@ -6389,19 +6384,19 @@ impl Announced { } /// The epoch the advertisement at `path` named, if any. - fn epoch(&self, path: &PathOwned) -> Option { + fn epoch(&self, path: &Path) -> Option { self.epochs.get(path).cloned() } /// Record the epoch a restart of the live advertisement at `path` names. - fn restart(&mut self, path: &PathOwned, epoch: Option) { + fn restart(&mut self, path: &Path, epoch: Option) { match epoch { Some(epoch) => self.epochs.insert(path.clone(), epoch), None => self.epochs.remove(path), }; } - fn attached(&mut self, path: &PathOwned) -> Option<&mut AnnouncedRoute> { + fn attached(&mut self, path: &Path) -> Option<&mut AnnouncedRoute> { match self.routes.get_mut(path)? { Held::Attached(route) => Some(route.as_mut()), _ => None, @@ -6410,7 +6405,7 @@ impl Announced { /// Retire this session's advertisement without invalidating another live /// session from the same peer. Dropping its sources closes their requests. - fn withdraw(&mut self, path: &PathOwned) { + fn withdraw(&mut self, path: &Path) { self.epochs.remove(path); let Some(held) = self.routes.remove(path) else { return; @@ -6425,7 +6420,7 @@ impl Announced { /// path, answer the requester with its consumer, and hand the source to the /// driver's serve machines, which own it from then on. fn poll_serve(&mut self, subscriber: &Subscriber, waiter: &kio::Waiter) { - let root = subscriber.origin.root().to_owned(); + let root = subscriber.origin.root(); while let Poll::Ready(Ok(path)) = self.ready.poll_pop(waiter) { // A route retired since it woke has nothing left to serve. let Some(Held::Attached(entry)) = self.routes.get_mut(&path) else { @@ -6438,11 +6433,11 @@ impl Announced { while let Poll::Ready(Ok(request)) = entry.dynamic.poll_requested_broadcast(route_waiter) { // The request path is absolute; the wire (and our origin handle) // speak paths relative to the session's root. - let Some(path) = request.path().strip_prefix(&root) else { + let Some(path) = request.path().strip_prefix(root) else { // Outside our root: nothing we could name on the wire. continue; }; - let path = path.to_owned(); + let path = path.clone(); // A request naming the epoch reaches only that instance, so only a source // without one must stop once the peer no longer selects what minted it. let validity = if entry.route.epoch.is_some() { @@ -6450,7 +6445,7 @@ impl Announced { } else { crate::model::broadcast::RequestValidity::for_source(&entry.dynamic, request.path()) }; - let source = subscriber.origin.create_source(&path, validity); + let source = subscriber.origin.create_source(path.clone(), validity); // The handler exists before the requester sees the source, so a track it // asks for before the serve machine first runs queues instead of failing // `NotFound`. Accepted before the push, so the requester holds the source @@ -6472,7 +6467,7 @@ impl Announced { /// its sources (tracks in flight end on their own gates, with `Unauthorized`), and /// attach every withheld one it now does. fn limit(&mut self, permit: &crate::auth::Permit, subscriber: &Subscriber) { - let changed: Vec = self + let changed: Vec = self .routes .iter() .filter(|(path, held)| match held { @@ -6524,7 +6519,7 @@ struct AnnouncedRoute { /// Whether the GOAWAY drain already re-priced this route. drained: bool, /// Queues this route on its prefix's ready set when its request queue wakes. - wake: Arc>, + wake: Arc>, waker: std::task::Waker, /// Keeps this route's registration on its request queue alive between passes. park: kio::Park, @@ -6534,7 +6529,7 @@ impl AnnouncedRoute { fn new( route: crate::origin::Route, dynamic: crate::origin::Requests, - wake: Arc>, + wake: Arc>, ) -> Self { Self { route, @@ -6589,7 +6584,7 @@ struct TrackServe { /// The advertisement that minted this source, absent for standalone test sources. route: Option>, subscriber: Subscriber, - path: PathOwned, + path: Path, /// The publisher instance to ask for; the peer refuses a request for another. epoch: Option, name: String, @@ -6905,7 +6900,7 @@ impl Establish { let msg = lite::Subscribe { epoch: self.serve.epoch.clone(), id: self.id, - broadcast: self.serve.path.as_path(), + broadcast: self.serve.path.clone(), track: self.serve.name.as_str().into(), priority: self.subscription.priority, max_delay: wire_max_delay(self.subscription.max_delay), @@ -7253,7 +7248,7 @@ impl TrackInfoFetch { stream.writer.buffer(&lite::ControlType::Track)?; stream.writer.buffer(&lite::Track { epoch: serve.epoch.clone(), - broadcast: serve.path.as_path(), + broadcast: serve.path.clone(), track: serve.name.as_str().into(), })?; self.state = TrackInfoState::Send { stream }; @@ -8250,7 +8245,7 @@ impl kio::Task for FetchServeRun { let msg = lite::Fetch { epoch: self.serve.epoch.clone(), - broadcast: self.serve.path.as_path(), + broadcast: self.serve.path.clone(), track: self.serve.name.as_str().into(), priority: request.priority(), // Older wires cannot carry it; the local cache judges it instead. diff --git a/rs/moq-net/src/lite/track.rs b/rs/moq-net/src/lite/track.rs index 372324f4f1f..61a72eaa746 100644 --- a/rs/moq-net/src/lite/track.rs +++ b/rs/moq-net/src/lite/track.rs @@ -16,7 +16,7 @@ const LEGACY_UNLIMITED: u64 = (1u64 << 53) - 1; /// Lite05+ only. #[derive(Clone, Debug)] pub struct Track<'a> { - pub broadcast: Path<'a>, + pub broadcast: Path, /// The publisher instance the subscriber expects; see [`crate::origin::Route::epoch`]. /// Lite07+ only. pub epoch: Option, @@ -483,7 +483,7 @@ mod test { fn track_request_roundtrips_on_lite05() { let msg = Track { epoch: None, - broadcast: Path::new("room").to_owned(), + broadcast: Path::from_static("room"), track: Cow::Borrowed("video"), }; let mut buf = Vec::new(); @@ -491,7 +491,7 @@ mod test { .unwrap(); let mut slice = buf.as_slice(); let got = crate::coding::decode_buf(&mut slice, Version::Lite05, Track::decode_msg).unwrap(); - assert_eq!(got.broadcast, Path::new("room")); + assert_eq!(got.broadcast, Path::from_static("room")); assert_eq!(got.track, "video"); } @@ -499,7 +499,7 @@ mod test { fn track_request_errors_before_lite05() { let msg = Track { epoch: None, - broadcast: Path::new("room").to_owned(), + broadcast: Path::from_static("room"), track: Cow::Borrowed("video"), }; let mut buf = Vec::new(); diff --git a/rs/moq-net/src/lite/tracks.rs b/rs/moq-net/src/lite/tracks.rs index 5b7124386df..a724984190c 100644 --- a/rs/moq-net/src/lite/tracks.rs +++ b/rs/moq-net/src/lite/tracks.rs @@ -17,13 +17,13 @@ const SYNCED: u64 = 0x4; /// /// Lite07+ only. #[derive(Clone, Debug)] -pub struct Tracks<'a> { - pub broadcast: Path<'a>, +pub struct Tracks { + pub broadcast: Path, /// The publisher instance the subscriber expects; see [`crate::origin::Route::epoch`]. pub epoch: Option, } -impl Message for Tracks<'_> { +impl Message for Tracks { const CONTROL: Option = Some(crate::stats::Message::TrackInfo); fn decode_msg(r: &mut Decoder<'_>, version: Version) -> Result { if !version.has_tracks() { @@ -196,19 +196,19 @@ mod test { fn the_request_roundtrips() { let epoch: crate::Epoch = "01900000-0000-7000-8000-000000000001".parse().unwrap(); let msg = Tracks { - broadcast: Path::new("room/alice").to_owned(), + broadcast: Path::from_static("room/alice"), epoch: Some(epoch.clone()), }; let buf = msg.encode_bytes(Version::Lite07).unwrap(); let (got, _) = Tracks::decode_slice(&buf, Version::Lite07).unwrap(); - assert_eq!(got.broadcast, Path::new("room/alice")); + assert_eq!(got.broadcast, Path::from_static("room/alice")); assert_eq!(got.epoch, Some(epoch)); } #[test] fn older_versions_have_no_tracks_stream() { let msg = Tracks { - broadcast: Path::new("room").to_owned(), + broadcast: Path::from_static("room"), epoch: None, }; assert!(msg.encode_bytes(Version::Lite06).is_err()); diff --git a/rs/moq-net/src/model/broadcast.rs b/rs/moq-net/src/model/broadcast.rs index c1af9979672..123d8f294c3 100644 --- a/rs/moq-net/src/model/broadcast.rs +++ b/rs/moq-net/src/model/broadcast.rs @@ -56,7 +56,7 @@ pub struct Info { /// /// Empty (the default) for a standalone broadcast with no origin, which is then its own /// root: any `..` reference escapes. - pub path: crate::PathOwned, + pub path: crate::Path, /// The epoch of the route a [`Consumer`] was resolved through, naming the publisher /// instance it reads. @@ -75,7 +75,7 @@ impl Default for Info { Self { pool: cache::Pool::new(cache::Config::default().with_expiry(cache::DEFAULT_EXPIRY)), cache_duration: std::time::Duration::MAX, - path: crate::PathOwned::default(), + path: crate::Path::default(), epoch: None, frames: Default::default(), } @@ -892,7 +892,7 @@ impl Consumer { /// references resolve against is where the *reader* found the broadcast, not where its /// producer happened to create it. Free when the two already agree, which is the case /// for a broadcast created at the path an unrooted cursor asks for. - pub(super) fn with_path(mut self, path: crate::PathOwned) -> Self { + pub(super) fn with_path(mut self, path: crate::Path) -> Self { if self.info.path != path { let mut info = (*self.info).clone(); info.path = path; diff --git a/rs/moq-net/src/model/follow.rs b/rs/moq-net/src/model/follow.rs index 696dee7d29e..43763970379 100644 --- a/rs/moq-net/src/model/follow.rs +++ b/rs/moq-net/src/model/follow.rs @@ -2,7 +2,7 @@ use std::task::{Poll, ready}; use super::WeakEntry; use super::origin_impl::{Announce, AnnounceConsumer, AnnounceEvent, Consumer, Routing}; -use crate::{Epoch, Error, PathOwned, broadcast}; +use crate::{Epoch, Error, Path, broadcast}; /// The announcements covering one path, as one broadcast coming online, being replaced, and /// going offline. Created by [`Consumer::follow`](crate::origin::Consumer::follow). @@ -15,7 +15,7 @@ use crate::{Epoch, Error, PathOwned, broadcast}; /// ignored. pub struct Follow { announced: AnnounceConsumer, - path: PathOwned, + path: Path, /// Every route standing over the path. covering: Vec, /// How many events the cursor has delivered. @@ -40,7 +40,7 @@ struct Standing { } impl Follow { - pub(super) fn new(announced: AnnounceConsumer, path: PathOwned) -> Self { + pub(super) fn new(announced: AnnounceConsumer, path: Path) -> Self { Self { announced, path, @@ -195,7 +195,7 @@ impl Follow { /// [`Consumer::announced_broadcast`](crate::origin::Consumer::announced_broadcast). pub struct AnnouncedBroadcast { consumer: Consumer, - path: PathOwned, + path: Path, /// The only instance handed out, if pinned. epoch: Option, follow: Follow, @@ -209,7 +209,7 @@ pub struct AnnouncedBroadcast { } impl AnnouncedBroadcast { - pub(super) fn new(consumer: Consumer, path: PathOwned, epoch: Option, follow: Follow) -> Self { + pub(super) fn new(consumer: Consumer, path: Path, epoch: Option, follow: Follow) -> Self { Self { consumer, path, diff --git a/rs/moq-net/src/model/group.rs b/rs/moq-net/src/model/group.rs index a7e465c9233..a4cf6b74707 100644 --- a/rs/moq-net/src/model/group.rs +++ b/rs/moq-net/src/model/group.rs @@ -5232,7 +5232,7 @@ mod test { #[cfg(feature = "async")] fn inflation_is_shared_and_stops_at_release() { let registry = stats::Registry::new(stats::Config::new()); - let meter = registry.publisher_meter("demo", &stats::Tier::default()); + let meter = registry.publisher_meter(crate::Path::from_static("demo"), &stats::Tier::default()); let inflated = || { registry .snapshot() @@ -5296,7 +5296,7 @@ mod test { .map_or(0, |(_, _, traffic)| traffic.bytes) }; let pool = cache::Pool::unbounded(); - let meter = registry.publisher_meter("demo", &stats::Tier::default()); + let meter = registry.publisher_meter(crate::Path::from_static("demo"), &stats::Tier::default()); let mut group = compressed_group(&pool).with_meter(meter); group.write_frame(None, verbose(0)).unwrap(); diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index 295019a37a6..7591f2735d4 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -18,9 +18,9 @@ use super::{ front::{Action, Candidate, Event, Front, Refusal}, }; use crate::{ - AsPath, Epoch, Error, InvalidPattern, Path, PathOwned, Pattern, Patterns, SessionError, StreamError, + Epoch, Error, InvalidPattern, Path, Pattern, Patterns, SessionError, StreamError, coding::{BoundsExceeded, Decode, DecodeError, Decoder, Encode, EncodeError, Encoder, Form, varint}, - path::Segment, + path::{self, Segment}, time::{Deadlines, Instant}, util::{Keepalive, SendCell, TaskSet, Tasks, TasksWeak}, }; @@ -880,18 +880,18 @@ struct Pending { /// what changed or came due since the last one, never the entries it holds back. #[derive(Default)] struct OriginConsumerState { - pending: HashMap, + pending: HashMap, /// The entries a poll looks at next, in delivery order: queued or changed since, /// or come due. - due: BTreeMap, + due: BTreeMap, /// The entries held back, by when a poll looks at them again. - parked: BTreeMap<(Instant, u64), PathOwned>, + parked: BTreeMap<(Instant, u64), Path>, next_seq: u64, /// Prefixes whose most recently delivered update was an announce. A pending /// `Announce` is ambiguous on its own: it is an unseen initial announce (a /// retraction cancels it entirely) or a metadata update on a route the /// consumer already observed (a retraction must still be delivered). - delivered: BTreeSet, + delivered: BTreeSet, /// Set by the origin's teardown: the cursor drains `pending`, then reports /// the end instead of parking forever on a table that can never fire again. ended: bool, @@ -934,7 +934,7 @@ impl OriginConsumerState { /// Queue `update` at `prefix`, or replace the one queued there in its place. A /// replaced entry is looked at again on the next poll. - fn put(&mut self, prefix: PathOwned, update: PendingUpdate) { + fn put(&mut self, prefix: Path, update: PendingUpdate) { let Some(pending) = self.pending.get_mut(&prefix) else { self.insert(prefix, update, false); return; @@ -947,7 +947,7 @@ impl OriginConsumerState { } /// Queue `update` at `prefix`, which has nothing queued, last in delivery order. - fn insert(&mut self, prefix: PathOwned, update: PendingUpdate, owed: bool) { + fn insert(&mut self, prefix: Path, update: PendingUpdate, owed: bool) { let seq = self.next_seq; self.next_seq += 1; self.due.insert(seq, prefix.clone()); @@ -964,7 +964,7 @@ impl OriginConsumerState { } /// Unqueue the entry at `prefix`. - fn remove(&mut self, prefix: &PathOwned) -> Option { + fn remove(&mut self, prefix: &Path) -> Option { let pending = self.pending.remove(prefix)?; match pending.wake { Some(wake) => self.parked.remove(&(wake, pending.seq)), @@ -982,7 +982,7 @@ impl OriginConsumerState { } } - fn apply_announce(&mut self, prefix: PathOwned, meta: RouteMeta, captures: Option>) { + fn apply_announce(&mut self, prefix: Path, meta: RouteMeta, captures: Option>) { let meta = (meta, captures); let new = match self.pending.get_mut(&prefix).and_then(|pending| pending.update.take()) { // First announce, a stale announce being replaced, or a metadata update. @@ -1002,7 +1002,7 @@ impl OriginConsumerState { /// unless this one goes at once. fn apply_restart( &mut self, - prefix: PathOwned, + prefix: Path, meta: RouteMeta, captures: Option>, release: Option, @@ -1022,7 +1022,7 @@ impl OriginConsumerState { self.put(prefix, new); } - fn apply_unannounce(&mut self, prefix: PathOwned, last: RouteMeta, captures: Option>) { + fn apply_unannounce(&mut self, prefix: Path, last: RouteMeta, captures: Option>) { let last = (last, captures); match self.pending.get_mut(&prefix).and_then(|pending| pending.update.take()) { // The pending announce was never delivered and neither was any earlier @@ -1069,10 +1069,10 @@ impl OriginConsumerState { /// `retracted`, settled against `now`. fn scan( &mut self, - retracted: &HashMap, + retracted: &HashMap, now: Instant, hold: Duration, - ) -> Result> { + ) -> Result> { while let Some(entry) = self.parked.first_entry() && entry.key().0 <= now { @@ -1133,7 +1133,7 @@ impl OriginConsumerState { } /// Take the pending update at `prefix` for delivery. - fn take_prefix(&mut self, prefix: PathOwned) -> AnnounceEvent { + fn take_prefix(&mut self, prefix: Path) -> AnnounceEvent { let (event, owed) = self.take_update(prefix); let (AnnounceEvent::Start(announce) | AnnounceEvent::Update(announce) @@ -1146,7 +1146,7 @@ impl OriginConsumerState { event } - fn take_update(&mut self, prefix: PathOwned) -> (AnnounceEvent, bool) { + fn take_update(&mut self, prefix: Path) -> (AnnounceEvent, bool) { let pending = self.remove(&prefix).unwrap(); let owed = pending.owed; let ((meta, captures), kind): (_, fn(Announce) -> AnnounceEvent) = match pending.update.unwrap() { @@ -1229,7 +1229,7 @@ struct RouteEntry { epoch: Option, /// Names the content behind the route when it has no epoch; see [`Instance`]. generation: u64, - prefix: PathOwned, + prefix: Path, /// The absolute patterns the announcing producer may serve. The prefix is /// only the wire-visible covering claim; this scope remains authoritative. scope: Patterns, @@ -1382,14 +1382,14 @@ fn prefix_claim(prefix: &Path) -> Result { struct ServeState { // Result channels for pending requests, keyed by absolute path so concurrent // `request_broadcast` calls for the same path coalesce onto one channel. - requests: RequestQueue>, + requests: RequestQueue>, // Broadcasts the handler has already served, kept weakly so a repeat request for the // same path resolves to a shared clone instead of re-invoking the handler (which would // open a duplicate upstream subscription). Weak so a served broadcast still closes once // its real consumers drop. The cache reclaims closed entries incrementally on insert, so a // long-lived origin serving many distinct one-shot paths stays bounded by the live count. - served: WeakCache, + served: WeakCache, // How many times the route changed instance: a front compares it to the count when // it asked to tell a released request from an answered one. Shared outside the lock, @@ -1407,7 +1407,7 @@ struct ServeState { /// a route flowing back through one of its readers, nor a local view a peer's /// route. Every other requester shares the plain front, so fronts scale with /// the peers in the path's route chains rather than with viewer sessions. -type FrontKey = (PathOwned, Horizon); +type FrontKey = (Path, Horizon); /// Which routes a reader sees: the split-horizon exclusion and the local-only view. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] @@ -1504,7 +1504,7 @@ struct CursorRoute { /// Whether it serves requests: part of the dedupe key, but never delivered. served: bool, /// The absolute prefix it was announced at, which decides `captures`. - prefix: PathOwned, + prefix: Path, /// What the cursor's scope captured from `prefix`. captures: Option>, /// [`OriginState::next_route`] as it started. @@ -1525,13 +1525,13 @@ impl WeakEntry for RemoteFront { /// re-rooted, and the per-cursor delivery buffer. struct TableCursor { /// The prefix stripped from every delivered path. - root: PathOwned, + root: Path, /// The absolute patterns this cursor is scoped to (its token / scope). allowed: Patterns, /// Where the cursor hangs in the [`RouteTable`]: the literal heads of /// `allowed`. A route the cursor can see sits at or under one of them, or on /// the walk down to one. - heads: Vec, + heads: Vec, /// The routes this cursor may see (control-plane split horizon). horizon: Horizon, /// Which routes beneath a hidden segment are reported. @@ -1541,10 +1541,10 @@ struct TableCursor { state: kio::Producer, /// Where this cursor's presented prefixes land in the delivery buffer: empty, /// or the mount the cursor reads for, relative to the consumer's root. - under: PathOwned, + under: Path, /// The absolute prefixes a mount shadows: routes at or beneath them are not /// this cursor's to present. - holes: Vec, + holes: Vec, /// For a cursor reading through a mount: the mount and the handle's own /// patterns, so a wildcard captures what the handle names rather than the target. named: Option<(Mount, Patterns)>, @@ -1552,7 +1552,7 @@ struct TableCursor { interested: bool, /// The last delivered best route per presented (relative) prefix, for change /// detection. Servability is part of the dedupe key but never leaves the model. - current: HashMap, + current: HashMap, } impl TableCursor { @@ -1561,15 +1561,15 @@ impl TableCursor { /// `claim` is the prefix's [`prefix_claim`], which the caller already holds: /// building one allocates, and the sweeps below ask this per route per /// cursor. - fn presented(&self, prefix: &Path, claim: &Pattern) -> Option { + fn presented(&self, prefix: &Path, claim: &Pattern) -> Option { if !self.allowed.overlaps(claim) { return None; } if let Some(relative) = prefix.strip_prefix(&self.root) { - return Some(relative.to_owned()); + return Some(relative); } - self.root.has_prefix(prefix).then(PathOwned::default) + self.root.has_prefix(prefix).then(Path::default) } /// What the cursor's most specific matching scope member captures from an @@ -1615,12 +1615,12 @@ impl TableCursor { /// Deliver a changed winner at this presented prefix, as of `now`, the /// [`OriginState::next_route`]. - fn update(&mut self, presented: &PathOwned, best: Option<&RouteEntry>, now: u64, hold: &Hold) { + fn update(&mut self, presented: &Path, best: Option<&RouteEntry>, now: u64, hold: &Hold) { let Some(entry) = best else { if let Some(last) = self.current.remove(presented) && let Ok(mut state) = self.state.write() { - state.apply_unannounce(self.under.join(presented), last.meta, last.captures); + state.apply_unannounce(self.under.rejoin(presented), last.meta, last.captures); } return; }; @@ -1660,15 +1660,15 @@ impl TableCursor { // identity explicitly so consumers keyed on it remove it. Some(previous) if previous.captures != captures => { if let Ok(mut state) = self.state.write() { - state.apply_unannounce(self.under.join(presented), previous.meta, previous.captures); - state.apply_announce(self.under.join(presented), meta, captures); + state.apply_unannounce(self.under.rejoin(presented), previous.meta, previous.captures); + state.apply_announce(self.under.rejoin(presented), meta, captures); } } // Another instance: whatever was resolved under the prefix is stale. Some(previous) if previous.instance != instance => { let release = meta.3.is_none().then(|| hold.release()).flatten(); if let Ok(mut state) = self.state.write() { - state.apply_restart(self.under.join(presented), meta, captures, release); + state.apply_restart(self.under.rejoin(presented), meta, captures, release); } } // Unchanged metadata and servability: nothing the consumer could @@ -1680,7 +1680,7 @@ impl TableCursor { Some(previous) if previous.meta == meta && previous.served == served => {} _ => { if let Ok(mut state) = self.state.write() { - state.apply_announce(self.under.join(presented), meta, captures); + state.apply_announce(self.under.rejoin(presented), meta, captures); } } } @@ -1715,30 +1715,31 @@ struct OriginScope { /// `target/rest`. Both absolute. #[derive(Clone, Debug)] struct Mount { - at: PathOwned, - target: PathOwned, + at: Path, + target: Path, } impl Mount { /// Where the handle-side absolute `path` resolves, when it is under this mount - /// and the result fits [`Path::MAX_PARTS`]. - fn resolve(&self, path: &Path) -> Option { - let resolved = self.target.join(path.strip_prefix(&self.at)?); + /// and the result fits the wire limits. + fn resolve(&self, path: &Path) -> Option { + let resolved = self.target.join(&path.strip_prefix(&self.at)?).ok()?; (resolved.parts().count() <= Path::MAX_PARTS).then_some(resolved) } /// Where the origin-side absolute `path` shows on the handle, when it is at or /// beneath the target. A route covering the target has no handle-side name. - fn name(&self, path: &Path) -> Option { - Some(self.at.join(path.strip_prefix(&self.target)?)) + fn name(&self, path: &Path) -> Option { + self.at.join(&path.strip_prefix(&self.target)?).ok() } - /// Whether the origin-side absolute `path` has a handle-side name within - /// [`Path::MAX_PARTS`], so a reader could ask for it. A route covering the - /// target always does. + /// Whether the origin-side absolute `path` has a handle-side name within the + /// wire limits, so a reader could ask for it. A route covering the target always does. fn names(&self, path: &Path) -> bool { - path.strip_prefix(&self.target) - .is_none_or(|rest| self.at.parts().count() + rest.parts().count() <= Path::MAX_PARTS) + path.strip_prefix(&self.target).is_none_or(|rest| { + self.at.parts().count() + rest.parts().count() <= Path::MAX_PARTS + && self.at.len() + usize::from(!self.at.is_empty() && !rest.is_empty()) + rest.len() <= Path::MAX_BYTES + }) } /// The handle-side absolute `patterns` beneath this mount, as origin-side @@ -1769,9 +1770,9 @@ impl Mount { /// A handle-side interest head as an origin-side one: a head beneath the mount /// moves under the target, and one above it hangs at the target itself. - fn translate_head(&self, head: &Path) -> Option { + fn translate_head(&self, head: &Path) -> Option { match head.strip_prefix(&self.at) { - Some(rest) => Some(self.target.join(rest)), + Some(rest) => self.target.join(&rest).ok(), None => self.at.has_prefix(head).then(|| self.target.clone()), } } @@ -1806,11 +1807,11 @@ impl OriginScope { } /// Where the absolute `path` resolves on the origin: itself, or its mount - /// target. `None` when the mount would resolve it past [`Path::MAX_PARTS`]. - fn resolve<'a>(&self, path: &'a Path<'a>) -> Option> { + /// target. `None` when the mount would resolve it past the wire limits. + fn resolve(&self, path: &Path) -> Option { match self.mount(path) { Some(mount) => mount.resolve(path), - None => Some(path.borrow()), + None => Some(path.clone()), } } @@ -1842,10 +1843,11 @@ impl Default for OriginScope { /// The announce-interest prefixes that cover a pattern scope on a prefix-only /// wire: each member's literal head, minus heads another already covers. -pub(crate) fn interest_prefixes(allowed: &Patterns) -> Vec { - let mut heads: Vec = allowed +pub(crate) fn interest_prefixes(allowed: &Patterns) -> Vec { + // A head too long for a path covers none, so it asks for nothing. + let mut heads: Vec = allowed .iter() - .map(|pattern| Path::new(pattern.head()).to_owned()) + .filter_map(|pattern| Path::new(pattern.head()).ok()) .collect(); heads.sort(); heads.dedup(); @@ -1865,22 +1867,22 @@ pub(super) struct Hidden { /// Report hidden routes too. include: bool, /// Measure visibility from the request prefix instead of authorization heads. - from: Option, + 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>, + beyond: Option>, } impl Hidden { /// Whether a cursor hanging at `heads` reports a route at `prefix`. - fn discovers(&self, heads: &[PathOwned], prefix: &Path) -> bool { + fn discovers(&self, heads: &[Path], 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. - fn reaches(&self, heads: &[PathOwned], prefix: &Path) -> bool { + fn reaches(&self, heads: &[Path], prefix: &Path) -> bool { self.include || !hides(self.from.as_ref().map(std::slice::from_ref).unwrap_or(heads), prefix) } @@ -1901,7 +1903,7 @@ impl Hidden { /// Whether a segment of `prefix` below the head it sits under starts with `.`. /// A route at or above a head has nothing below it, so it never hides. -fn hides(heads: &[PathOwned], prefix: &Path) -> bool { +fn hides(heads: &[Path], prefix: &Path) -> bool { heads .iter() .any(|head| prefix.strip_prefix(head).is_some_and(|below| below.is_hidden())) @@ -1935,7 +1937,7 @@ pub enum AnnounceEvent { #[derive(Clone, Debug)] pub struct Announce { /// The prefix the route covers, relative to the consuming cursor's root. - pub prefix: PathOwned, + pub prefix: Path, /// What the scope's wildcards stood for when the announced prefix pins all of /// them. `None` for an overlap-only route or a scope without a complete match. pub captures: Option>, @@ -1956,7 +1958,7 @@ pub struct Producer { scope: OriginScope, // The prefix that is automatically stripped from all paths. - root: PathOwned, + root: Path, // The origin's shared state: the route table, announce cursors, and the // remotely-served fronts. Shared with every derived consumer. @@ -2012,7 +2014,7 @@ impl Producer { hop: config.hop, max_hops: config.max_hops, scope: scope.clone(), - root: PathOwned::default(), + root: Path::default(), shared: shared.clone(), pool: config.pool, cache_duration: config.cache_duration, @@ -2119,7 +2121,7 @@ impl Producer { hop, scope: OriginScope::empty(), max_hops: HopLimit::default(), - root: PathOwned::default(), + root: Path::default(), shared: SendCell::new(kio::Shared::new(OriginState::new( DEFAULT_UPDATE_HOLD, Deadlines::new(Instant::ZERO), @@ -2163,12 +2165,12 @@ impl Producer { /// producer may publish under (after [`scope`](Self::scope)) or beneath a /// [`mount`](Self::mount), /// [`Error::BoundsExceeded`] if the full rooted path exceeds - /// [`Path::MAX_PARTS`], [`Error::InvalidPath`] if it holds a segment no + /// [`Path::MAX_PARTS`] or 16 KiB, [`Error::InvalidPath`] if it holds a segment no /// pattern can spell (`*` or `**`), [`Error::NavigationSegment`] for a literal `.` or `..`, /// or [`Error::Closed`] once the origin's /// [`Driver`] has been dropped. - pub fn create_broadcast(&self, path: impl AsPath) -> Result { - let full = self.root.join(path.as_path()).to_owned(); + pub fn create_broadcast(&self, path: impl TryInto>) -> Result { + let full = self.root.join(&path::convert(path)?)?; let (broadcast, announcer) = self.create_withheld(full)?; Ok(Publication { broadcast: broadcast.with_announcer(announcer), @@ -2177,7 +2179,7 @@ impl Producer { /// A new broadcast at `full` with its announcer withheld, so it cannot announce /// until the announcer is handed back. - fn create_withheld(&self, full: PathOwned) -> Result<(broadcast::Producer, Announcer), Error> { + fn create_withheld(&self, full: Path) -> Result<(broadcast::Producer, Announcer), Error> { publish_path(&full)?; if !self.scope.permits(&full) || !self.scope.publishes(&full) { return Err(Error::Unauthorized); @@ -2235,7 +2237,7 @@ impl Producer { } /// Create and advertise a broadcast in one call. - pub fn publish(&self, path: impl AsPath, route: Route) -> Result { + pub fn publish(&self, path: impl TryInto>, route: Route) -> Result { let broadcast = self.create_broadcast(path)?; broadcast.announce(route)?; Ok(broadcast) @@ -2250,11 +2252,11 @@ impl Producer { #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] pub(crate) fn create_source( &self, - path: impl AsPath, + path: Path, validity: crate::model::broadcast::RequestValidity, ) -> broadcast::Producer { - let path = path.as_path(); - let full = self.root.join(&path).to_owned(); + // The path came off a served request, whose root join already fit. + let full = self.root.join(&path).expect("request path fits"); let ingress = self.stats.ingress(&full); broadcast::Info { pool: self.pool.clone(), @@ -2277,7 +2279,11 @@ impl Producer { /// advertises through a broadcast ([`Publication::announce`]) or a /// [`Requests`] handler, which serve what they claim. #[cfg(test)] - pub(crate) fn announce(&self, prefix: impl AsPath, route: Route) -> Result { + pub(crate) fn announce( + &self, + prefix: impl TryInto>, + route: Route, + ) -> Result { Announcing::new(self, prefix)?.announce( route, Serving { @@ -2292,8 +2298,11 @@ impl Producer { /// synced until the returned guard drops. Taken before the request goes out, so /// nothing reads as synced in between. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - pub(crate) fn snapshot(&self, prefix: impl AsPath) -> Snapshot { - let prefix = self.root.join(prefix.as_path()).to_owned(); + pub(crate) fn snapshot(&self, prefix: impl TryInto) -> Snapshot { + let prefix = self + .root + .join(&prefix.try_into().expect("snapshot prefix")) + .expect("snapshot prefix fits"); self.shared.lock().snapshot(&self.shared, prefix, self.link) } @@ -2319,7 +2328,11 @@ impl Producer { false => { for prefix in state.routes.interests_from(&Path::empty()) { for relative in listener.asks(&prefix) { - let snapshot = state.snapshot(&self.shared, self.root.join(&relative), self.link); + // A head too long to root covers no path, so nothing reads it. + let Ok(absolute) = self.root.join(&relative) else { + continue; + }; + let snapshot = state.snapshot(&self.shared, absolute, self.link); let _ = queue.try_push(InterestChange { prefix: relative, wanted: true, @@ -2367,17 +2380,26 @@ impl Producer { /// /// The prefix must overlap this producer's pattern scope. Individual requests /// remain authoritative and are refused when they do not match the scope. - pub fn requests(&self, prefix: impl AsPath) -> Result { + pub fn requests(&self, prefix: impl TryInto>) -> Result { self.requests_inner(prefix, Route::default(), true) } /// Land a peer's claim with its wire route metadata. #[cfg(any(feature = "async", test))] - pub(crate) fn requests_route(&self, prefix: impl AsPath, route: Route) -> Result { + pub(crate) fn requests_route( + &self, + prefix: impl TryInto>, + route: Route, + ) -> Result { self.requests_inner(prefix, route, false) } - fn requests_inner(&self, prefix: impl AsPath, route: Route, publishes: bool) -> Result { + fn requests_inner( + &self, + prefix: impl TryInto>, + route: Route, + publishes: bool, + ) -> Result { let announcing = Announcing::new(self, prefix)?; let serve = kio::Shared::::default(); serve.lock().requests.add_handler(); @@ -2403,8 +2425,8 @@ impl Producer { /// the new root. Returns [`Error::Unauthorized`] when the requested scope has /// no overlap with this producer's scope, or [`Error::BoundsExceeded`] when /// rooting the patterns would exceed the path limit. - pub fn scope(&self, root: impl AsPath, patterns: &Patterns) -> Result { - let root = self.root.join(root).to_owned(); + pub fn scope(&self, root: impl TryInto>, patterns: &Patterns) -> Result { + let root = self.root.join(&path::convert(root)?)?; let rooted = patterns.rooted(root.as_str()).map_err(|_| BoundsExceeded)?; let scope = self.scope.narrow(&rooted).ok_or(Error::Unauthorized)?; Ok(Producer { @@ -2440,15 +2462,16 @@ impl Producer { /// `at` or `target` overlaps a mount point this producer already has, or `at` /// overlaps an existing target or its own: mounts never chain, in whatever order they are added, /// so a target is always read as the origin holds it. Mounts may share a target. - /// [`Error::BoundsExceeded`] if either rooted path exceeds [`Path::MAX_PARTS`], + /// [`Error::BoundsExceeded`] if either rooted path exceeds [`Path::MAX_PARTS`] or 16 KiB, /// and [`Error::InvalidPath`] if `at` holds a segment no pattern can spell. - pub fn mount(&self, at: impl AsPath, target: impl AsPath) -> Result { - let at = self.root.join(at).to_owned(); - let target = self.root.join(target).to_owned(); - if [&at, &target] - .into_iter() - .any(|path| path.parts().count() > Path::MAX_PARTS) - { + pub fn mount( + &self, + at: impl TryInto>, + target: impl TryInto>, + ) -> Result { + let at = self.root.join(&path::convert(at)?)?; + let target = self.root.join(&path::convert(target)?)?; + if at.parts().count() > Path::MAX_PARTS || target.parts().count() > Path::MAX_PARTS { return Err(BoundsExceeded.into()); } // A mount point no pattern can spell could never be announced or authorized. @@ -2500,7 +2523,7 @@ impl Producer { } /// Returns the root that is automatically stripped from all paths. - pub fn root(&self) -> &PathOwned { + pub fn root(&self) -> &Path { &self.root } @@ -2510,8 +2533,8 @@ impl Producer { } /// Converts a relative path to an absolute path. - pub fn absolute(&self, path: impl AsPath) -> PathOwned { - self.root.join(path) + pub fn absolute(&self, path: impl TryInto>) -> Result { + Ok(self.root.join(&path::convert(path)?)?) } } @@ -2522,11 +2545,11 @@ struct Announcing { hop: Hop, shared: SendCell>, /// The absolute advertised prefix, which also keys the ingress announce counters. - requested: PathOwned, + requested: Path, /// The prefix inserted into the table, with its [`prefix_claim`]. Pattern /// scopes decide visibility and request authorization without changing the /// route's prefix shape. - prefixes: Vec<(PathOwned, Pattern)>, + prefixes: Vec<(Path, Pattern)>, /// The absolute paths the producer is authorized to serve. scope: Patterns, local: bool, @@ -2539,8 +2562,8 @@ struct Announcing { impl Announcing { /// The requested prefix as-is, refused when its subtree does not overlap the scope. - fn new(producer: &Producer, prefix: impl AsPath) -> Result { - let requested = producer.root.join(prefix.as_path()).to_owned(); + fn new(producer: &Producer, prefix: impl TryInto>) -> Result { + let requested = producer.root.join(&path::convert(prefix)?)?; publish_path(&requested)?; if requested.parts().count() > Path::MAX_PARTS { return Err(BoundsExceeded.into()); @@ -2723,7 +2746,7 @@ pub(crate) struct AnnounceProducer { shared: SendCell>, /// The table entries this advertisement created, by prefix and id. A prefix /// remains unchanged; pattern scopes only filter its visibility and requests. - entries: Vec<(PathOwned, u64)>, + entries: Vec<(Path, u64)>, /// Ingress announce stats guard, held only while the entries are advertised. guard: Option, } @@ -3035,7 +3058,7 @@ struct FrontTask { /// The broadcast the front serves. broadcast: broadcast::Producer, /// Absolute path of the front. - path: PathOwned, + path: Path, /// The requesters' horizon, applied to every (re)selection. horizon: Horizon, /// Wakes the front when a route covering its path changes. @@ -3319,9 +3342,9 @@ async fn serve_front(task: FrontTask) -> Vec { } // Read alongside the decision, under the lock a poke takes first. *seen = watch.seen(); - front.retain_routes(|route| table.routes.covers(&path.as_path(), route)); + front.retain_routes(|route| table.routes.covers(&path, route)); let instance = resolved.as_ref().map(|resolved| &resolved.instance); - let best = pick(&table, &path.as_path(), horizon, front, instance); + let best = pick(&table, &path, horizon, front, instance); // Under the table's lock, which a request reads it under too: a request that // sees another winner from here on mints its own front rather than join this one. if resolved.is_none() @@ -3347,7 +3370,7 @@ async fn serve_front(task: FrontTask) -> Vec { let joined = joined.load(Ordering::Acquire); let renew = resolved .as_mut() - .and_then(|resolved| resolved.moved(&table, &path.as_path(), horizon, joined)); + .and_then(|resolved| resolved.moved(&table, &path, horizon, joined)); drop(table); if let Some(prefix) = renew { // Checked under the lock that renews: a restart after the read covers the move. @@ -3369,7 +3392,7 @@ async fn serve_front(task: FrontTask) -> Vec { let standing = |front: &Front, route: u64, resolved: &Option| { let table = shared.read(); let instance = resolved.as_ref().map(|resolved| &resolved.instance); - pick(&table, &path.as_path(), horizon, front, instance).is_some_and(|best| best.id == route) + pick(&table, &path, horizon, front, instance).is_some_and(|best| best.id == route) }; // Whether an answer `route` gave for `asked` would serve another instance than the @@ -3377,7 +3400,7 @@ async fn serve_front(task: FrontTask) -> Vec { // instance won the path while the request was in flight. let stale = |front: &Front, route: u64, asked: &Instance, resolved: &Option| { let table = shared.read(); - let Some(entry) = table.routes.covering(&path.as_path()).find(|entry| entry.id == route) else { + let Some(entry) = table.routes.covering(&path).find(|entry| entry.id == route) else { return false; }; if entry.instance() != *asked { @@ -3386,7 +3409,7 @@ async fn serve_front(task: FrontTask) -> Vec { match resolved { Some(resolved) => resolved.instance != *asked, None => table - .best_route(&path.as_path(), horizon, front.excluded_routes(), |_| true) + .best_route(&path, horizon, front.excluded_routes(), |_| true) .is_some_and(|best| best.instance() != *asked), } }; @@ -3410,7 +3433,7 @@ async fn serve_front(task: FrontTask) -> Vec { let table = shared.read(); table .routes - .covering(&path.as_path()) + .covering(&path) .find(|entry| entry.id == route && entry.live()) .map(|entry| (entry.source.clone(), entry.server.clone(), entry.instance())) }; @@ -3537,7 +3560,7 @@ async fn serve_front(task: FrontTask) -> Vec { let prefix = shared .read() .routes - .covering(&path.as_path()) + .covering(&path) .find(|entry| entry.id == route) .map(|entry| entry.prefix.clone()); resolved = Some(Resolution { @@ -4017,7 +4040,7 @@ async fn serve_front(task: FrontTask) -> Vec { shared .read() .routes - .covering(&path.as_path()) + .covering(&path) .find(|entry| entry.id == route) .is_some_and(|entry| entry.prefix != path) }); @@ -4152,11 +4175,11 @@ enum WatchProducer { } struct Selected { - path: PathOwned, + path: Path, via: Hop, instance: Instance, /// The prefix of the route the instance came through. - prefix: PathOwned, + prefix: Path, /// The selection period, closed when the peer stops selecting the instance. current: Option>, /// Open until another instance of the peer takes the path, which may answer a @@ -4241,7 +4264,7 @@ impl Selected { #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] pub(crate) struct Selection { shared: SendCell>, - path: PathOwned, + path: Path, id: u64, signal: kio::Consumer, } @@ -4295,7 +4318,7 @@ impl Drop for Selection { /// holding it. struct Watch { shared: SendCell>, - path: PathOwned, + path: Path, id: u64, signal: kio::Consumer, } @@ -4333,7 +4356,7 @@ struct SnapshotEntry { /// The prefix's [`prefix_claim`], so a cursor whose patterns miss it never waits on it. claim: Pattern, /// The absolute prefix, so a cursor that could see nothing beneath it never waits on it. - prefix: PathOwned, + prefix: Path, } /// A session still delivering its peer's initial announcements under a prefix. @@ -4343,7 +4366,7 @@ struct SnapshotEntry { /// has inserted the announcements its peer counted, or it gave up on them. pub(crate) struct Snapshot { shared: Option>>, - prefix: PathOwned, + prefix: Path, id: u64, } @@ -4353,7 +4376,7 @@ impl Snapshot { pub(crate) fn landed() -> Self { Self { shared: None, - prefix: PathOwned::default(), + prefix: Path::default(), id: 0, } } @@ -4372,12 +4395,12 @@ impl Drop for Snapshot { /// Dropping it takes the table lock: never drop one while holding it. struct Interest { shared: SendCell>, - prefix: PathOwned, + prefix: Path, } impl Interest { /// Count `prefix` under the held table lock. - fn new(state: &mut OriginState, shared: &SendCell>, prefix: PathOwned) -> Self { + fn new(state: &mut OriginState, shared: &SendCell>, prefix: Path) -> Self { state.add_interest(shared, &prefix); Self { shared: shared.clone(), @@ -4396,9 +4419,9 @@ impl Drop for Interest { struct Listener { id: u64, /// The session's root, absolute: what it asks about is named relative to it. - root: PathOwned, + root: Path, /// The broadest prefixes the session may ask about, relative to `root`. - heads: Vec, + heads: Vec, /// The absolute patterns the session may publish under. allowed: Patterns, /// The link the session's routes arrive on, which its snapshots wait for. @@ -4410,7 +4433,7 @@ impl Listener { /// What the session asks about, relative to its root, for an interest at the /// absolute `prefix`: each head an interest above it covers, since the scope /// may ask for no more, or the prefix itself inside a head. - fn asks(&self, prefix: &Path) -> Vec { + fn asks(&self, prefix: &Path) -> Vec { let Some(relative) = prefix.strip_prefix(&self.root) else { return match self.root.has_prefix(prefix) { true => self.heads.clone(), @@ -4424,7 +4447,7 @@ impl Listener { } else if relative.has_prefix(head) { let reaches = Pattern::subtree(prefix.as_str()).is_ok_and(|subtree| self.allowed.overlaps(&subtree)); return match reaches { - true => vec![relative.to_owned()], + true => vec![relative], false => Vec::new(), }; } @@ -4464,7 +4487,7 @@ impl Drop for Interests { )] pub(crate) struct InterestChange { /// The prefix, relative to the session's root. - pub prefix: PathOwned, + pub prefix: Path, /// Whether readers now want the prefix discovered, or no longer do. pub wanted: bool, /// Set when wanted: holds the readers under the prefix short of synced from @@ -4640,12 +4663,12 @@ impl RouteNode { /// Collect the broadest interests strictly beneath this node, which sits at /// `at`: each one no other interest beneath this node covers. - fn collect_interests(&self, at: &Path, out: &mut Vec) { + fn collect_interests(&self, at: &Path, out: &mut Vec) { for (part, child) in &self.children { if child.interests_below == 0 { continue; } - let path = at.join(part.as_str()); + let path = at.rejoin(&Path::new(part).expect("a segment of a path fits")); match child.interests > 0 { true => out.push(path), false => child.collect_interests(&path, out), @@ -4827,12 +4850,12 @@ impl RouteTable { /// The interests at `path` or, failing that, the broadest beneath it: what a /// session joining now asks about under `path`. - fn interests_from(&self, path: &Path) -> Vec { + fn interests_from(&self, path: &Path) -> Vec { let Some(node) = self.root.find(path.parts()) else { return Vec::new(); }; if node.interests > 0 { - return vec![path.to_owned()]; + return vec![path.clone()]; } let mut out = Vec::new(); node.collect_interests(path, &mut out); @@ -4902,7 +4925,7 @@ struct OriginState { // Peers that withdrew a prefix while routes there still passed through them, // which are stale. See [`Requests::withdrawn`]. - withdrawn: HashMap>, + withdrawn: HashMap>, // Set when the origin's driver dropped: new requests fail with `Closed` // immediately and handlers observe the end instead of parking forever. @@ -4961,7 +4984,7 @@ impl OriginState { /// A route at `prefix` was announced or restarted with `hops`. When its sender, /// the chain's last hop, had withdrawn the prefix, routes through it are live /// once more. - fn reannounced(&mut self, prefix: &PathOwned, hops: &Hops) { + fn reannounced(&mut self, prefix: &Path, hops: &Hops) { if let Some(sender) = hops.iter().last() && self.withdrawn.get_mut(prefix).is_some_and(|peers| peers.remove(sender)) { @@ -4971,7 +4994,7 @@ impl OriginState { } /// Forget the withdrawals at `prefix` that no longer hide a route. - fn prune_withdrawn(&mut self, prefix: &PathOwned) { + fn prune_withdrawn(&mut self, prefix: &Path) { if self.withdrawn.is_empty() { return; } @@ -5058,7 +5081,7 @@ impl OriginState { } /// Start a [`Snapshot`] at the absolute `prefix` for the routes arriving on `link`. - fn snapshot(&mut self, shared: &SendCell>, prefix: PathOwned, link: Link) -> Snapshot { + fn snapshot(&mut self, shared: &SendCell>, prefix: Path, link: Link) -> Snapshot { // A prefix past the depth limit can carry no route, so nothing waits on it. let Ok(claim) = prefix_claim(&prefix) else { return Snapshot::landed(); @@ -5133,12 +5156,11 @@ impl OriginState { let mut changes = Vec::new(); for listener in &self.listeners { for relative in listener.asks(prefix) { - changes.push(( - listener.queue.clone(), - listener.root.join(&relative), - relative, - listener.link, - )); + // A head too long to root covers no path, so nothing reads it. + let Ok(absolute) = listener.root.join(&relative) else { + continue; + }; + changes.push((listener.queue.clone(), absolute, relative, listener.link)); } } for (queue, absolute, prefix, link) in changes { @@ -5160,7 +5182,7 @@ impl OriginState { let signal = self.routes.add_watch(path, id); Watch { shared: shared.clone(), - path: path.to_owned(), + path: path.clone(), id, signal, } @@ -5168,7 +5190,7 @@ impl OriginState { /// Recompute the best visible route presenting at `presented` (relative) for /// one cursor and deliver the change, if any, as of `now`. - fn sync_cursor(routes: &RouteTable, cursor: &mut TableCursor, presented: &PathOwned, now: u64, hold: &Hold) { + fn sync_cursor(routes: &RouteTable, cursor: &mut TableCursor, presented: &Path, now: u64, hold: &Hold) { // The entries presenting here are the ones announced at the absolute // prefix, or, for the cursor's own root, at the root and every prefix // above it (all of which present as the empty path). Among them, the @@ -5177,7 +5199,7 @@ impl OriginState { let best = match presented.is_empty() { true => cursor.best(routes.covering(&cursor.root)), false => { - let absolute = cursor.root.join(presented); + let absolute = cursor.root.rejoin(presented); cursor.best(routes.at(&absolute)) } }; @@ -5202,7 +5224,7 @@ impl OriginState { // The routes a cursor can see sit on the walk down to one of its heads or // somewhere beneath it, so only those subtrees are replayed. - let mut presented: BTreeSet = BTreeSet::new(); + let mut presented: BTreeSet = BTreeSet::new(); for head in &cursor.heads { let (above, at) = self.routes.split(head); let mut nodes = above; @@ -5380,7 +5402,7 @@ struct Resolution { instance: Instance, route: u64, /// That route's prefix, when it was still in the table as the front resolved. - prefix: Option, + prefix: Option, /// The [`RemoteFront::joined`] the prefix last restarted for the path's move, by /// a renewal or on its own; see [`Self::moved`]. settled: Option, @@ -5396,7 +5418,7 @@ impl Resolution { /// /// Settled once per join: a renewal leaves the front's instance behind, so /// nobody joins it after. - fn moved(&mut self, table: &OriginState, path: &Path, horizon: Horizon, joined: u64) -> Option { + fn moved(&mut self, table: &OriginState, path: &Path, horizon: Horizon, joined: u64) -> Option { if self.settled == Some(joined) || !matches!(self.instance, Instance::Route(_)) { return None; } @@ -5462,7 +5484,7 @@ impl Requests { .best_route(path, Horizon::default(), &HashSet::new(), |entry| entry.via == via) .is_some_and(|entry| entry.instance() == instance); let producer = kio::Producer::new(Selected { - path: path.to_owned(), + path: path.clone(), via, instance, prefix, @@ -5481,7 +5503,7 @@ impl Requests { .push((id, WatchProducer::Selection(producer))); Some(Selection { shared: shared.clone(), - path: path.to_owned(), + path: path.clone(), id, signal, }) @@ -5583,7 +5605,7 @@ impl ServeState { /// still held. fn resolve( shared: &kio::Shared, - path: &PathOwned, + path: &Path, producer: &kio::Producer, result: Result, ) -> bool { @@ -5602,7 +5624,7 @@ impl ServeState { /// its requesters resolve with, or `None` once it is no longer ours to answer. fn answer( &mut self, - path: &PathOwned, + path: &Path, producer: &kio::Producer, result: Result, ) -> Option> { @@ -5651,7 +5673,7 @@ impl ServeState { } /// Drop the still-pending entry, if it is still ours. - fn forget(shared: &kio::Shared, path: &PathOwned, producer: &kio::Producer) { + fn forget(shared: &kio::Shared, path: &Path, producer: &kio::Producer) { shared.lock().requests.remove_if(path, |p| p.same_channel(producer)); } } @@ -5672,7 +5694,7 @@ impl ServeState { /// ``` pub struct Request { // Absolute path that was requested. - path: PathOwned, + path: Path, // Result channel back to the awaiting requester(s). Writing `resolved` and dropping // this wakes them with the outcome. @@ -5689,7 +5711,7 @@ pub struct Request { impl Request { /// The absolute path that was requested. - pub fn path(&self) -> &PathOwned { + pub fn path(&self) -> &Path { &self.path } @@ -5793,7 +5815,7 @@ pub struct Requesting { // The path the requester asked for, relative to its cursor's root. Stamped on the // resolved broadcast (see [`broadcast::Info::path`]) because a handler is free to // serve a broadcast created somewhere else entirely, or at no path at all. - path: PathOwned, + path: Path, // Egress scope applied to the resolved broadcast, so its reads are attributed. // Empty (no-op) for an untagged consumer. stats: stats::Scope, @@ -5838,7 +5860,7 @@ impl Requesting { fn new(inner: RequestState) -> Self { Self { inner, - path: PathOwned::default(), + path: Path::default(), stats: stats::Scope::default(), epoch: None, } @@ -5850,7 +5872,7 @@ impl Requesting { self } - fn with_path(mut self, path: PathOwned) -> Self { + fn with_path(mut self, path: Path) -> Self { self.path = path; self } @@ -5904,7 +5926,7 @@ impl kio::Task for Requesting { /// The pollable result of [`Consumer::routed_broadcast`]. pub struct Routing { consumer: Consumer, - path: PathOwned, + path: Path, // Asks sessions to discover the path for as long as this waits on it. interest: Option, // The covering routes, watched since the generation the last ask saw. @@ -5943,21 +5965,24 @@ impl kio::Task for Routing { if table.closed { return Poll::Ready(Err(Error::Closed)); } - let named = self.consumer.root.join(&self.path); + let named = match self.consumer.root.join(&self.path) { + Ok(named) => named, + Err(err) => return Poll::Ready(Err(err.into())), + }; let Some(resolved) = self.consumer.scope.resolve(&named) else { return Poll::Ready(Err(BoundsExceeded.into())); }; let watch = table.watch(&self.consumer.shared, &resolved); let seen = watch.seen(); if self.interest.is_none() && self.consumer.interested { - let resolved = resolved.to_owned(); + let resolved = resolved.clone(); self.interest = Some(Interest::new(&mut table, &self.consumer.shared, resolved)); } drop(table); self.watch = Some((watch, seen)); self.request = Some( self.consumer - .request(self.path.as_path(), self.epoch.as_ref()) + .request(self.path.clone(), self.epoch.as_ref()) .into_inner(), ); } @@ -6036,7 +6061,7 @@ pub struct Consumer { scope: OriginScope, // A prefix that is automatically stripped from all paths. - root: PathOwned, + root: Path, // The origin's shared state: the route table, announce cursors, and the // remotely-served fronts. @@ -6231,24 +6256,22 @@ impl Consumer { /// Drop the returned [`AnnounceConsumer`] to unregister. pub fn announced(&self) -> AnnounceConsumer { let state = kio::Producer::::default(); - let cursor = |root: PathOwned, - allowed: Patterns, - hidden: Hidden, - mount: Option<&Mount>, - under: PathOwned, - holes: Vec| TableCursor { - root, - heads: interest_prefixes(&allowed), - allowed, - horizon: self.horizon, - hidden, - state: state.clone(), - under, - holes, - named: mount.map(|mount| (mount.clone(), self.scope.allowed.clone())), - interested: self.interested, - current: HashMap::new(), - }; + let cursor = + |root: Path, allowed: Patterns, hidden: Hidden, mount: Option<&Mount>, under: Path, holes: Vec| { + TableCursor { + root, + heads: interest_prefixes(&allowed), + allowed, + horizon: self.horizon, + hidden, + state: state.clone(), + under, + holes, + named: mount.map(|mount| (mount.clone(), self.scope.allowed.clone())), + interested: self.interested, + current: HashMap::new(), + } + }; // A root at or beneath a mount reads only the mount: one cursor, re-rooted // onto the target. @@ -6269,7 +6292,7 @@ impl Consumer { mount.translate(&self.scope.allowed), self.hidden.translate(mount), Some(mount), - PathOwned::default(), + Path::default(), Vec::new(), )]; return AnnounceConsumer::new( @@ -6308,7 +6331,7 @@ impl Consumer { allowed, self.hidden.translate(mount), Some(mount), - under.to_owned(), + under.clone(), Vec::new(), )); } @@ -6319,7 +6342,7 @@ impl Consumer { self.scope.allowed.clone(), self.hidden.clone(), None, - PathOwned::default(), + Path::default(), holes, ), ); @@ -6341,8 +6364,8 @@ impl Consumer { /// The newest broadcast published on this origin at exactly `path`, if any. /// Test-only: a request goes through the table like any other. #[cfg(test)] - pub(crate) fn get_broadcast(&self, path: impl AsPath) -> Option { - let full = self.root.join(path).to_owned(); + pub(crate) fn get_broadcast(&self, path: impl TryInto>) -> Option { + let full = self.root.join(&path::convert::(path).ok()?).ok()?; if !self.scope.permits(&full) { return None; } @@ -6366,7 +6389,7 @@ impl Consumer { /// [`Self::routed_broadcast`]: pairing this with [`Self::request_broadcast`] /// leaves a gap where the covering route can retract. #[cfg(feature = "async")] - pub async fn routed(&self, path: impl AsPath) -> Option { + pub async fn routed(&self, path: impl TryInto>) -> Option { // A follower's first event is always a start. match self.follow(path).ok()?.next().await? { AnnounceEvent::Start(announce) | AnnounceEvent::Update(announce) | AnnounceEvent::Restart(announce) => { @@ -6385,8 +6408,8 @@ impl Consumer { /// over, which subscriptions already ride out. Fails with [`Error::Unauthorized`] when /// this consumer's scope can never cover the path, and with [`Error::InvalidPath`] when no /// pattern can spell it (a segment containing `*`). - pub fn follow(&self, path: impl AsPath) -> Result { - let path = path.as_path(); + pub fn follow(&self, path: impl TryInto>) -> Result { + let path = path::convert(path)?; // Scope down to the path itself, which still sees every route whose claim covers it: // the rest of the origin never wakes it, and a route claiming only paths beneath it @@ -6404,7 +6427,7 @@ impl Consumer { // the announce guards. Hiding narrows discovery, not lookup, so a hidden path // follows like any other. let announced = consumer.untagged().with_hidden(true).announced(); - Ok(crate::announce::Follow::new(announced, path.to_owned())) + Ok(crate::announce::Follow::new(announced, path)) } /// Resolve `path` to a broadcast: [`Self::request_broadcast`], retried @@ -6420,12 +6443,23 @@ impl Consumer { /// [`Error::Unauthorized`] for a path outside this consumer's scope, /// [`Error::Closed`] once the origin closes, and any other resolution /// failure as-is. Await it, or poll it through [`kio::Task`]. - pub fn routed_broadcast(&self, path: impl AsPath) -> kio::Pending { - kio::Pending::new(self.routing(path.as_path().to_owned(), None)) + pub fn routed_broadcast(&self, path: impl TryInto>) -> kio::Pending { + let routing = match path::convert(path) { + Ok(path) => self.routing(path, None), + Err(err) => Routing { + consumer: self.clone(), + path: Path::empty(), + interest: None, + watch: None, + request: Some(Requesting::failed(err)), + epoch: None, + }, + }; + kio::Pending::new(routing) } /// [`Self::routed_broadcast`], refusing any instance but `epoch` when one is pinned. - pub(super) fn routing(&self, path: PathOwned, epoch: Option) -> Routing { + pub(super) fn routing(&self, path: Path, epoch: Option) -> Routing { // `allowed` keeps narrower permissions intact: if the whole path is not // reachable, no route can ever cover it, so bail rather than loop forever. let request = (!self.allowed().matches(path.as_str())).then(|| Requesting::failed(Error::Unauthorized)); @@ -6455,17 +6489,12 @@ impl Consumer { /// cover the path, and with [`Error::InvalidPath`] when no pattern can spell it. pub fn announced_broadcast( &self, - path: impl AsPath, + path: impl TryInto>, epoch: impl Into>, ) -> Result { - let path = path.as_path(); + let path = path::convert(path)?; let follow = self.follow(&path)?; - Ok(AnnouncedBroadcast::new( - self.clone(), - path.to_owned(), - epoch.into(), - follow, - )) + Ok(AnnouncedBroadcast::new(self.clone(), path, epoch.into(), follow)) } /// Returns a consumer rooted at `root` and restricted to matching `patterns`. @@ -6474,8 +6503,8 @@ impl Consumer { /// the new root. Returns [`Error::Unauthorized`] when the requested scope has /// no overlap with this consumer's scope, or [`Error::BoundsExceeded`] when /// rooting the patterns would exceed the path limit. - pub fn scope(&self, root: impl AsPath, patterns: &Patterns) -> Result { - let root = self.root.join(root).to_owned(); + pub fn scope(&self, root: impl TryInto>, patterns: &Patterns) -> Result { + let root = self.root.join(&path::convert(root)?)?; let rooted = patterns.rooted(root.as_str()).map_err(|_| BoundsExceeded)?; let scope = self.scope.narrow(&rooted).ok_or(Error::Unauthorized)?; Ok(Consumer { @@ -6528,26 +6557,32 @@ impl Consumer { /// [`Error::NotFound`] on its tracks instead. pub fn request_broadcast( &self, - path: impl AsPath, + path: impl TryInto>, epoch: impl Into>, ) -> kio::Pending { let epoch = epoch.into(); - self.request(path.as_path(), epoch.as_ref()) + match path::convert(path) { + Ok(path) => self.request(path, epoch.as_ref()), + Err(err) => kio::Pending::new(Requesting::failed(err)), + } } /// [`Self::request_broadcast`], refused with [`Error::Unroutable`] unless the /// winning route serves `epoch`: a peer asking for one publisher instance must /// not be handed another. - pub(crate) fn request(&self, path: Path<'_>, epoch: Option<&crate::Epoch>) -> kio::Pending { + pub(crate) fn request(&self, path: Path, epoch: Option<&crate::Epoch>) -> kio::Pending { // The path as this handle names it, absolute: what its scope authorizes and // what its egress is counted under, so a read through a mount bills where // the reader asked. - let named = self.root.join(&path).to_owned(); + let named = match self.root.join(&path) { + Ok(named) => named, + Err(err) => return kio::Pending::new(Requesting::failed(err.into())), + }; let scope = self.stats.egress(&named); // The resolved handle is named by what *this* cursor asked for, not by the absolute // path: a rooted cursor cannot name anything above its own root, so that is what a // catalog it reads may reference. - let requested = path.to_owned(); + let requested = path; // Routes only cover paths within this consumer's scope. if !self.scope.permits(&named) { @@ -6556,7 +6591,7 @@ impl Consumer { // Key requests by the absolute path on the origin, past any mount, so scoped, // rooted, and mounted consumers and handlers agree on the same entry and front. - let Some(absolute) = self.scope.resolve(&named).map(|path| path.to_owned()) else { + let Some(absolute) = self.scope.resolve(&named) else { return kio::Pending::new(Requesting::failed(BoundsExceeded.into())); }; @@ -6567,12 +6602,12 @@ impl Consumer { return kio::Pending::new(Requesting::failed(Error::Closed)); } - let horizon = self.horizon.effective(&state.routes, &absolute.as_path()); + let horizon = self.horizon.effective(&state.routes, &absolute); // Nothing serves the path: no announced broadcast and no served route. // Checked before joining a front, so a front still draining after its // route retracted takes no newcomers. - let best = match state.best_route(&absolute.as_path(), horizon, &HashSet::new(), |_| true) { + let best = match state.best_route(&absolute, horizon, &HashSet::new(), |_| true) { Some(best) if epoch.is_none_or(|epoch| best.epoch.as_ref() == Some(epoch)) => best.instance(), _ => return kio::Pending::new(Requesting::failed(Error::Unroutable)), }; @@ -6667,7 +6702,7 @@ impl Consumer { } /// Returns the prefix that is automatically stripped from all paths. - pub fn root(&self) -> &PathOwned { + pub fn root(&self) -> &Path { &self.root } @@ -6677,8 +6712,8 @@ impl Consumer { } /// Converts a relative path to an absolute path. - pub fn absolute(&self, path: impl AsPath) -> PathOwned { - self.root.join(path) + pub fn absolute(&self, path: impl TryInto>) -> Result { + Ok(self.root.join(&path::convert(path)?)?) } } @@ -6691,7 +6726,7 @@ pub struct AnnounceConsumer { // consumer reads through a mount. ids: Vec, shared: SendCell>, - root: PathOwned, + root: Path, // Pending updates queued for this cursor. Coalesced so a slow consumer // can't accumulate redundant announce/retract pairs. @@ -6704,7 +6739,7 @@ pub struct AnnounceConsumer { // Live egress announce guards, keyed by absolute prefix. An announce // opens one (bumping `announces_started` + `announced_bytes`); the matching retraction // drops it (bumping `announces_ended` + `announced_bytes`). - guards: HashMap, + guards: HashMap, // Holds the waiter a `Stream` poll registered; disjoint from `state` so the // borrow never collides with the body's. @@ -6716,9 +6751,9 @@ pub struct AnnounceConsumer { update_hold: Duration, // Quarantines by prefix, and their retractions in clock order: first waiting // for a fresh reading (by the stale one), then quarantined (by when each ends). - retracted: HashMap, - anchoring: VecDeque<(Instant, PathOwned)>, - expiring: VecDeque<(Instant, PathOwned)>, + retracted: HashMap, + anchoring: VecDeque<(Instant, Path)>, + expiring: VecDeque<(Instant, Path)>, timers: Deadlines, hold: crate::time::Deadline, } @@ -6740,16 +6775,23 @@ impl AnnounceConsumer { } #[cfg(test)] - pub(crate) async fn expect_next_active(&mut self, expected: impl AsPath) -> Route { + pub(crate) async fn expect_next_active(&mut self, expected: impl TryInto) -> Route { let event = self.expect_next().await; - Self::active(event, &expected.as_path()) + Self::active(event, &expected.try_into().expect("expected path")) } #[cfg(test)] - pub(crate) async fn expect_next_restarted(&mut self, expected: impl AsPath) -> Route { + pub(crate) async fn expect_next_restarted( + &mut self, + expected: impl TryInto, + ) -> Route { match self.expect_next().await { AnnounceEvent::Restart(announce) => { - assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"); + assert_eq!( + announce.prefix, + expected.try_into().expect("expected path"), + "wrong prefix" + ); announce.route } other => panic!("should be a restart: got {other:?}"), @@ -6757,15 +6799,19 @@ impl AnnounceConsumer { } #[cfg(test)] - pub(crate) async fn expect_next_ended(&mut self, expected: impl AsPath) { + pub(crate) async fn expect_next_ended(&mut self, expected: impl TryInto) { match self.expect_next().await { - AnnounceEvent::End(announce) => assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"), + AnnounceEvent::End(announce) => assert_eq!( + announce.prefix, + expected.try_into().expect("expected path"), + "wrong prefix" + ), other => panic!("should be a retraction: got {other:?}"), } } fn new( - root: PathOwned, + root: Path, cursors: Vec, state: kio::Producer, stats: stats::Session, @@ -6885,7 +6931,7 @@ impl AnnounceConsumer { fn hand_out(&mut self, event: AnnounceEvent) -> AnnounceEvent { match &event { AnnounceEvent::Start(announce) | AnnounceEvent::Update(announce) | AnnounceEvent::Restart(announce) => { - let scope = self.stats.egress(self.root.join(&announce.prefix)); + let scope = self.stats.egress(self.root.rejoin(&announce.prefix)); self.guards .entry(announce.prefix.clone()) .or_insert_with(|| scope.announce()); @@ -7033,11 +7079,7 @@ impl AnnounceConsumer { // A wider wire grant must reuse pending slots, rather than bypass the initial queue. #[cfg(feature = "async")] - pub(crate) fn replay<'a>( - &mut self, - advertised: impl IntoIterator, - revoked: &HashSet, - ) { + pub(crate) fn replay<'a>(&mut self, advertised: impl IntoIterator, revoked: &HashSet) { let advertised: HashSet<_> = advertised.into_iter().collect(); let table = self.shared.lock(); let Ok(mut state) = self.state.write() else { @@ -7048,7 +7090,7 @@ impl AnnounceConsumer { continue; }; for (presented, CursorRoute { meta, captures, .. }) in &cursor.current { - let prefix = cursor.under.join(presented); + let prefix = cursor.under.rejoin(presented); if revoked.contains(&prefix) { state.apply_unannounce(prefix, meta.clone(), captures.clone()); continue; @@ -7073,13 +7115,13 @@ impl AnnounceConsumer { } /// Returns the root that is automatically stripped from emitted prefixes. - pub fn root(&self) -> &PathOwned { + pub fn root(&self) -> &Path { &self.root } /// Converts an emitted prefix back to one rooted at the origin. - pub fn absolute(&self, prefix: impl AsPath) -> PathOwned { - self.root.join(prefix) + pub fn absolute(&self, prefix: impl TryInto>) -> Result { + Ok(self.root.join(&path::convert(prefix)?)?) } } @@ -7128,29 +7170,33 @@ impl AnnounceConsumer { } /// The next update must be an active route at `expected`; returns it. - pub fn assert_next_active(&mut self, expected: impl AsPath) -> Route { + pub fn assert_next_active(&mut self, expected: impl TryInto) -> Route { let event = kio::wait(|waiter| self.poll_next(waiter)) .now_or_never() .expect("next blocked") .expect("no next"); - Self::active(event, &expected.as_path()) + Self::active(event, &expected.try_into().expect("expected path")) } /// The `try_next` counterpart of [`Self::assert_next_active`]. - pub fn assert_try_next_active(&mut self, expected: impl AsPath) -> Route { + pub fn assert_try_next_active(&mut self, expected: impl TryInto) -> Route { let event = self.try_next().expect("no next"); - Self::active(event, &expected.as_path()) + Self::active(event, &expected.try_into().expect("expected path")) } /// The next update must be a restart at `expected`; returns its route. - pub fn assert_next_restarted(&mut self, expected: impl AsPath) -> Route { + pub fn assert_next_restarted(&mut self, expected: impl TryInto) -> Route { match kio::wait(|waiter| self.poll_next(waiter)) .now_or_never() .expect("next blocked") .expect("no next") { AnnounceEvent::Restart(announce) => { - assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"); + assert_eq!( + announce.prefix, + expected.try_into().expect("expected path"), + "wrong prefix" + ); announce.route } other => panic!("should be a restart: got {other:?}"), @@ -7158,13 +7204,17 @@ impl AnnounceConsumer { } /// The next update must be a retraction at `expected`. - pub fn assert_next_ended(&mut self, expected: impl AsPath) { + pub fn assert_next_ended(&mut self, expected: impl TryInto) { match kio::wait(|waiter| self.poll_next(waiter)) .now_or_never() .expect("next blocked") .expect("no next") { - AnnounceEvent::End(announce) => assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"), + AnnounceEvent::End(announce) => assert_eq!( + announce.prefix, + expected.try_into().expect("expected path"), + "wrong prefix" + ), other => panic!("should be a retraction: got {other:?}"), } } @@ -7244,7 +7294,7 @@ impl TestSelection { /// What a request for `room/a` through the selected route must still hold. pub(crate) fn validity(&self) -> crate::model::broadcast::RequestValidity { - crate::model::broadcast::RequestValidity::for_source(self.route.as_ref().unwrap(), &Path::new("room/a")) + crate::model::broadcast::RequestValidity::for_source(self.route.as_ref().unwrap(), &Path::from_static("room/a")) } /// The peer withdraws the route. @@ -7966,7 +8016,11 @@ mod tests { let producer = origin(1).produce(); let _foo = producer.publish(".svc/p1/foo", Route::default()).unwrap(); let project = mounted(&producer, "p1", &["**"]) - .with_stats(registry.tier(stats::Tier::default()).session("p1")) + .with_stats( + registry + .tier(stats::Tier::default()) + .session(crate::Path::from_static("p1")), + ) .with_hidden(true); let mut announced = project.announced(); @@ -8196,9 +8250,9 @@ mod tests { let table = producer.shared.read(); (0..PATHS) .map(|i| { - let path = Path::new(&format!("pool/job-{i}")).to_owned(); + let path = Path::new(&format!("pool/job-{i}")).unwrap(); let entry = table - .best_route(&path.as_path(), Horizon::default(), &HashSet::new(), |_| true) + .best_route(&path, Horizon::default(), &HashSet::new(), |_| true) .expect("the pool serves every path"); entry.hops.iter().next().copied().unwrap() }) @@ -9028,9 +9082,11 @@ mod tests { #[moq_clock::test] async fn announce_stats_follow_the_advertisement() { let registry = stats::Registry::new(stats::Config::new()); - let producer = origin(1) - .produce() - .with_stats(registry.tier(stats::Tier::default()).session("root")); + let producer = origin(1).produce().with_stats( + registry + .tier(stats::Tier::default()) + .session(crate::Path::from_static("root")), + ); let announces = || { registry .snapshot() @@ -9246,7 +9302,7 @@ mod tests { let watches = table .routes .root - .find(Path::new("room/alice").parts()) + .find(Path::from_static("room/alice").parts()) .map_or(0, |node| node.watches.len()); (table.fronts.len(), watches) }; @@ -9413,7 +9469,7 @@ mod tests { let producer = origin(1).produce(); let consumer = producer.consume(); let _published = producer.publish("room/alice", Route::default()).unwrap(); - let path = consumer.absolute("room/alice").to_owned(); + let path = consumer.absolute("room/alice").unwrap(); let broadcast = broadcast::Info::new().produce(); // Held, so the front waits on its holder rather than retiring on its own. let holder = broadcast.consume(); @@ -9800,7 +9856,7 @@ mod tests { } #[moq_clock::test] - async fn create_broadcast_accepts_a_max_depth_path() { + async fn create_broadcast_accepts_a_path_at_the_wire_limits() { let producer = origin(1).produce(); let path = vec!["a"; Path::MAX_PARTS].join("/"); let _broadcast = producer.create_broadcast(path.as_str()).expect("max depth is allowed"); @@ -9809,6 +9865,18 @@ mod tests { producer.create_broadcast(deeper.as_str()), Err(Error::BoundsExceeded(_)) )); + + // The byte limit counts the root the producer joins on. + let rooted = producer.scope("r", &Patterns::from(Pattern::all())).unwrap(); + let longest = "b".repeat(Path::MAX_BYTES - 2); + let _long = rooted + .create_broadcast(longest.as_str()) + .expect("max length is allowed"); + let longer = "c".repeat(Path::MAX_BYTES - 1); + assert!(matches!( + rooted.create_broadcast(longer.as_str()), + Err(Error::BoundsExceeded(_)) + )); } #[moq_clock::test] @@ -10305,7 +10373,7 @@ mod tests { for (i, route) in far.into_iter().enumerate().rev() { drop(route); match announced.try_next() { - Some(AnnounceEvent::End(end)) => assert_eq!(end.prefix, Path::new(&format!("room/{i}"))), + Some(AnnounceEvent::End(end)) => assert_eq!(end.prefix, Path::new(&format!("room/{i}")).unwrap()), other => panic!("should be a retraction: got {other:?}"), } } @@ -10489,7 +10557,7 @@ mod tests { let _relayed = peer .requests_route("room", Route::default().with_hops(hops(&[9, 2, 3]))) .unwrap(); - let path = Path::new("room/video"); + let path = Path::from_static("room/video"); let id = producer .shared .read() @@ -11185,7 +11253,7 @@ mod tests { .map(|i| format!("s{i}")) .collect::>() .join("/"); - assert_eq!(Path::new(&path).parts().count(), Path::MAX_PARTS); + assert_eq!(Path::new(&path).unwrap().parts().count(), Path::MAX_PARTS); assert!(consumer.allowed().matches(&path)); @@ -11562,7 +11630,7 @@ mod tests { matches!(pinned.await, Err(Error::Unroutable)), "a request pinned to the old epoch resolved" ); - let cached = dynamic.state.lock().served.get(&Path::new("room/alice").to_owned()); + let cached = dynamic.state.lock().served.get(&Path::from_static("room/alice")); assert!( cached.is_some_and(|cached| cached.consume().is_clone(&fresh.consume())), "the old answer was cached" @@ -12963,8 +13031,20 @@ mod tests { let route = producer.announce("room/a/b/c", Route::default()).unwrap(); { let table = producer.shared.lock(); - assert!(table.routes.root.find(Path::new("room/a/b/c").parts()).is_some()); - assert!(table.routes.root.find(Path::new("other/deep/head").parts()).is_some()); + assert!( + table + .routes + .root + .find(Path::from_static("room/a/b/c").parts()) + .is_some() + ); + assert!( + table + .routes + .root + .find(Path::from_static("other/deep/head").parts()) + .is_some() + ); assert_eq!(table.routes.root.cursors_below, 2); } @@ -13107,7 +13187,7 @@ mod tests { let broad = producer .requests_route("room", Route::default().with_via(origin(2))) .unwrap(); - let selected = broad.request_validity(&Path::new("room/a")).unwrap(); + let selected = broad.request_validity(&Path::from_static("room/a")).unwrap(); let pending = selected.current(); assert!(!pending.is_closed()); broad.update_route(broad.route().with_cost(Cost::from(7))).unwrap(); @@ -13138,7 +13218,7 @@ mod tests { #[moq_clock::test] async fn source_selection_answering_ends_only_on_a_replacement() { let producer = origin(1).produce(); - let path = Path::new("room/a"); + let path = Path::from_static("room/a"); let first = producer .requests_route("room", Route::default().with_via(origin(2))) .unwrap(); @@ -13176,7 +13256,7 @@ mod tests { let route = producer .requests_route("room", Route::default().with_via(origin(2))) .unwrap(); - let path = Path::new("room/a"); + let path = Path::from_static("room/a"); let selected = route.request_validity(&path).unwrap(); let lease = selected.current(); drop(selected); @@ -13189,7 +13269,10 @@ mod tests { async fn watch_wakes_only_for_covering_changes() { let producer = origin(1).produce(); let waiter = kio::Waiter::noop(); - let watch = producer.shared.lock().watch(&producer.shared, &Path::new("room/a")); + let watch = producer + .shared + .lock() + .watch(&producer.shared, &Path::from_static("room/a")); let seen = watch.seen(); // A route beside the path or beneath it covers nothing at the path. @@ -13217,7 +13300,7 @@ mod tests { let node = table .routes .root - .find(Path::new("room/a").parts()) + .find(Path::from_static("room/a").parts()) .expect("route below keeps the node"); assert!(node.watches.is_empty()); assert_eq!(table.routes.root.watches_below, 0); diff --git a/rs/moq-net/src/path/mod.rs b/rs/moq-net/src/path/mod.rs index 780d455c5dc..1056a051039 100644 --- a/rs/moq-net/src/path/mod.rs +++ b/rs/moq-net/src/path/mod.rs @@ -10,65 +10,62 @@ pub use moq_pattern::{InvalidPattern, Pattern, Patterns, Segment, Specificity}; -use std::borrow::Cow; use std::fmt::{self, Display}; use std::sync::Arc; use crate::coding::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}; -/// An owned version of [`Path`] with a `'static` lifetime. -pub type PathOwned = Path<'static>; +// Paths up to this many bytes live inline, in the word `Shared` holds beside its `Arc` less the +// length and the tag: 22 bytes on 64-bit targets and 10 on 32-bit ones. +const INLINE: usize = size_of::>() + size_of::() - 2; -/// A trait for types that can be converted to a `Path`. +/// Inline bytes, a static literal, or a suffix of a shared reference-counted buffer. /// -/// When providing a String/str, any leading/trailing slashes are trimmed and multiple consecutive slashes are collapsed. -/// When already a Path, normalization is skipped and the underlying buffer is reused without copying. -pub trait AsPath { - /// Borrow `self` as a [`Path`], normalizing slashes only when needed. - fn as_path(&self) -> Path<'_>; -} - -impl<'a> AsPath for &'a str { - fn as_path(&self) -> Path<'a> { - Path::new(self) - } -} - -impl<'a> AsPath for &'a Path<'a> { - fn as_path(&self) -> Path<'a> { - // We don't normalize again nor do we copy the bytes. - self.borrow() - } -} - -impl AsPath for Path<'_> { - fn as_path(&self) -> Path<'_> { - self.borrow() - } +/// Short paths copy inline, so cloning one is a memcpy with no atomics. A long path shares +/// one allocation: cloning bumps a refcount and suffix operations like `strip_prefix` only +/// advance `start`, so one allocation serves every copy of a path as it fans out. +#[derive(Clone)] +enum Repr { + Inline { len: u8, buf: [u8; INLINE] }, + Static(&'static str), + Shared { start: u16, buf: Arc }, } -impl AsPath for String { - fn as_path(&self) -> Path<'_> { - Path::new(self) - } -} +const _: () = assert!(size_of::() == INLINE + 2); +// Every path fits `MAX_BYTES`, so a `Shared` offset into one always fits its `u16`. +const _: () = assert!(Path::MAX_BYTES <= u16::MAX as usize); -impl<'a> AsPath for &'a String { - fn as_path(&self) -> Path<'a> { - Path::new(self) +impl Repr { + fn copy(s: &str) -> Result { + if s.len() > Path::MAX_BYTES { + return Err(TooLong); + } + Ok(Self::copy_bounded(s)) + } + + // `copy` for a string already known to fit `MAX_BYTES`. + fn copy_bounded(s: &str) -> Self { + debug_assert!(s.len() <= Path::MAX_BYTES); + if s.len() <= INLINE { + let mut buf = [0; INLINE]; + buf[..s.len()].copy_from_slice(s.as_bytes()); + Repr::Inline { + len: s.len() as u8, + buf, + } + } else { + Repr::Shared { + start: 0, + buf: s.into(), + } + } } } -/// A borrowed slice of the path, or a suffix of a shared reference-counted buffer. -/// -/// The `Shared` variant is what makes owned paths cheap: cloning bumps a refcount and -/// suffix operations (strip_prefix) only advance `start`, so one allocation -/// serves every copy of a path as it fans out to consumers. -#[derive(Clone)] -enum Repr<'a> { - Borrowed(&'a str), - Shared { buf: Arc, start: usize }, -} +/// A path or relative reference is longer than [`Path::MAX_BYTES`]. +#[derive(Debug, Copy, Clone, Eq, PartialEq, thiserror::Error)] +#[error("path is longer than {} bytes", Path::MAX_BYTES)] +pub struct TooLong; /// A broadcast path that provides safe prefix matching operations. /// @@ -76,36 +73,35 @@ enum Repr<'a> { /// delimiter boundaries, preventing issues like "foo" matching "foobar". /// /// Paths are automatically trimmed of leading and trailing slashes on creation, -/// making all slashes implicit at boundaries. A path names a point in the origin's +/// making all slashes implicit at boundaries. A path is at most [`Path::MAX_BYTES`] long, +/// so building a longer one fails with [`TooLong`]. A path names a point in the origin's /// tree from its root; a leading slash never escapes that root. The same type names /// an exact broadcast and the prefix a route or announcement covers, so a broadcast's /// own path is the prefix it announces. See [`Relative`] for `..`-style references. /// -/// Owned paths ([`PathOwned`]) share one reference-counted allocation: cloning, converting -/// a shared path with [`Path::to_owned`], and suffix operations like [`Path::strip_prefix`] -/// do not copy the underlying bytes. +/// Cloning is cheap: a short path is stored inline, and a long one shares a single +/// reference-counted allocation that suffix operations like [`Path::strip_prefix`] reuse. /// /// # Examples /// ``` -/// use moq_net::{Path}; +/// use moq_net::Path; /// /// // Creation automatically trims slashes -/// let path1 = Path::new("/foo/bar/"); -/// let path2 = Path::new("foo/bar"); +/// let path1 = Path::from_static("/foo/bar/"); +/// let path2 = Path::from_static("foo/bar"); /// assert_eq!(path1, path2); /// -/// // Methods accept both &str and Path -/// let base = Path::new("api/v1"); -/// assert!(base.has_prefix("api")); -/// assert!(base.has_prefix(&Path::new("api/v1"))); +/// let base = Path::from_static("api/v1"); +/// assert!(base.has_prefix(&Path::from_static("api"))); /// -/// let joined = base.join("users"); +/// let joined = base.join(&Path::from_static("users"))?; /// assert_eq!(joined.as_str(), "api/v1/users"); +/// # Ok::<(), moq_net::path::TooLong>(()) /// ``` #[derive(Clone)] -pub struct Path<'a>(Repr<'a>); +pub struct Path(Repr); -impl<'a> Path<'a> { +impl Path { /// Maximum number of slash-separated parts in a path. /// /// Matches the IETF moq-transport limit of 32 fields in a namespace tuple. @@ -113,51 +109,57 @@ impl<'a> Path<'a> { /// and publishing one to an origin is rejected. pub const MAX_PARTS: usize = 32; - /// Create a new Path from a string slice. + /// Maximum length of a path in bytes. + /// + /// A quarter of the 64 KiB moq-lite message limit, leaving room for every other + /// field of a message that carries a path. + pub const MAX_BYTES: usize = 16 * 1024; + + /// Create a new Path by copying a string slice. /// /// Leading and trailing slashes are automatically trimmed. /// Multiple consecutive internal slashes are collapsed to single slashes. - pub fn new(s: &'a str) -> Self { + /// Fails if the normalized path is longer than [`Path::MAX_BYTES`]. + pub fn new(s: &str) -> Result { let trimmed = s.trim_start_matches('/').trim_end_matches('/'); + match normalize(trimmed) { + Some(normalized) => Ok(Self(Repr::copy(&normalized)?)), + None => Ok(Self(Repr::copy(trimmed)?)), + } + } - // Check if we need to normalize (has multiple consecutive slashes) - if trimmed.contains("//") { - // Only allocate if we actually need to normalize - let normalized = trimmed - .split('/') - .filter(|s| !s.is_empty()) - .collect::>() - .join("/"); - Self(Repr::Shared { - buf: normalized.into(), - start: 0, - }) - } else { - // No normalization needed - use borrowed string - Self(Repr::Borrowed(trimmed)) + /// Create a new Path from a string literal without copying it, normalized like [`Path::new`]. + /// + /// # Panics + /// + /// If the normalized literal is longer than [`Path::MAX_BYTES`]. + pub fn from_static(s: &'static str) -> Self { + let trimmed = s.trim_start_matches('/').trim_end_matches('/'); + let normalized = normalize(trimmed); + let len = normalized.as_ref().map_or(trimmed.len(), String::len); + assert!(len <= Self::MAX_BYTES, "path literal is longer than Path::MAX_BYTES"); + match normalized { + Some(normalized) => Self(Repr::copy_bounded(&normalized)), + None => Self(Repr::Static(trimmed)), } } #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - pub(crate) fn from_escaped(s: String) -> PathOwned { - if s.is_empty() { - Path::empty() - } else { - Path(Repr::Shared { - buf: s.into(), - start: 0, - }) - } + pub(crate) fn from_escaped(s: String) -> Result { + Ok(Path(Repr::copy(&s)?)) } // A copy of this path skipping the first `n` bytes, reusing the shared buffer when possible. - pub(crate) fn slice_from(&'a self, n: usize) -> Path<'a> { + pub(crate) fn slice_from(&self, n: usize) -> Path { + let rest = &self.as_str()[n..]; match &self.0 { - Repr::Borrowed(s) => Path(Repr::Borrowed(&s[n..])), - Repr::Shared { buf, start } => Path(Repr::Shared { + Repr::Static(s) => Path(Repr::Static(&s[n..])), + Repr::Shared { buf, start } if rest.len() > INLINE => Path(Repr::Shared { buf: buf.clone(), - start: start + n, + // The offset stays within the buffer, which fits `MAX_BYTES` and so a `u16`. + start: *start + n as u16, }), + _ => Path(Repr::copy_bounded(rest)), } } @@ -173,63 +175,46 @@ impl<'a> Path<'a> { /// ``` /// use moq_net::Path; /// - /// let path = Path::new("foo/bar"); - /// assert!(path.has_prefix("foo")); - /// assert!(path.has_prefix(&Path::new("foo"))); - /// assert!(path.has_prefix("foo/")); - /// assert!(!path.has_prefix("fo")); + /// let path = Path::from_static("foo/bar"); + /// assert!(path.has_prefix(&Path::from_static("foo"))); + /// assert!(path.has_prefix(&Path::from_static("foo/"))); + /// assert!(!path.has_prefix(&Path::from_static("fo"))); /// - /// let path = Path::new("foobar"); - /// assert!(!path.has_prefix("foo")); + /// let path = Path::from_static("foobar"); + /// assert!(!path.has_prefix(&Path::from_static("foo"))); /// ``` - pub fn has_prefix(&self, prefix: impl AsPath) -> bool { - let prefix = prefix.as_path(); + pub fn has_prefix(&self, prefix: &Path) -> bool { + self.prefix_len(prefix).is_some() + } + // The bytes to skip past `prefix` and its delimiter, if it is a whole-segment prefix. + fn prefix_len(&self, prefix: &Path) -> Option { + let prefix = prefix.as_str(); if prefix.is_empty() { - return true; + return Some(0); } let s = self.as_str(); - if !s.starts_with(prefix.as_str()) { - return false; + if !s.starts_with(prefix) { + return None; } // Check if the prefix is the exact match if s.len() == prefix.len() { - return true; + return Some(s.len()); } // Otherwise, ensure the character after the prefix is a delimiter - s.as_bytes().get(prefix.len()) == Some(&b'/') + (s.as_bytes().get(prefix.len()) == Some(&b'/')).then_some(prefix.len() + 1) } /// The remainder after removing `prefix`, or `None` if it isn't a prefix. /// /// Only whole segments match: `a/bc` is not prefixed by `a/b`. An empty prefix /// returns the whole path. - pub fn strip_prefix(&'a self, prefix: impl AsPath) -> Option> { - let prefix = prefix.as_path(); - - if prefix.is_empty() { - return Some(self.borrow()); - } - - let s = self.as_str(); - if !s.starts_with(prefix.as_str()) { - return None; - } - - // Check if the prefix is the exact match - if s.len() == prefix.len() { - return Some(Path::empty()); - } - - // Otherwise, ensure the character after the prefix is a delimiter - if s.as_bytes().get(prefix.len()) != Some(&b'/') { - return None; - } - - Some(self.slice_from(prefix.len() + 1)) + pub fn strip_prefix(&self, prefix: &Path) -> Option { + let n = self.prefix_len(prefix)?; + Some(self.slice_from(n)) } /// Iterate over the slash-separated parts of the path. @@ -240,7 +225,7 @@ impl<'a> Path<'a> { /// ``` /// use moq_net::Path; /// - /// let path = Path::new("foo/bar/baz"); + /// let path = Path::from_static("foo/bar/baz"); /// assert_eq!(path.parts().collect::>(), ["foo", "bar", "baz"]); /// assert_eq!(Path::empty().parts().count(), 0); /// ``` @@ -259,14 +244,17 @@ impl<'a> Path<'a> { /// The normalized path as a string, with no leading or trailing slash. pub fn as_str(&self) -> &str { match &self.0 { - Repr::Borrowed(s) => s, - Repr::Shared { buf, start } => &buf[*start..], + // SAFETY: `Repr::copy` and `concat` fill `buf[..len]` only from whole `&str`s and an + // ASCII `/`, so those bytes are valid UTF-8. + Repr::Inline { len, buf } => unsafe { std::str::from_utf8_unchecked(&buf[..*len as usize]) }, + Repr::Static(s) => s, + Repr::Shared { buf, start } => &buf[*start as usize..], } } /// The empty path, which prefixes every other path. - pub fn empty() -> Path<'static> { - Path(Repr::Borrowed("")) + pub const fn empty() -> Path { + Path(Repr::Static("")) } /// Returns `true` if this is the empty path. @@ -279,64 +267,57 @@ impl<'a> Path<'a> { self.as_str().len() } - /// Clone into a `'static` path, sharing the existing buffer when there is one. - pub fn to_owned(&self) -> PathOwned { - match &self.0 { - Repr::Borrowed("") => Path::empty(), - Repr::Borrowed(s) => Path(Repr::Shared { - buf: Arc::from(*s), - start: 0, - }), - Repr::Shared { buf, start } => Path(Repr::Shared { - buf: buf.clone(), - start: *start, - }), - } - } - - /// Consume into a `'static` path, reusing the existing buffer when there is one. - pub fn into_owned(self) -> PathOwned { - match self.0 { - Repr::Borrowed("") => Path::empty(), - Repr::Borrowed(s) => Path(Repr::Shared { - buf: Arc::from(s), - start: 0, - }), - Repr::Shared { buf, start } => Path(Repr::Shared { buf, start }), - } - } - - /// A copy of this path bound to `self`'s lifetime, without copying the underlying bytes. - pub fn borrow(&'a self) -> Path<'a> { - self.slice_from(0) - } - /// Join this path with another path component. /// + /// Fails if the result is longer than [`Path::MAX_BYTES`]. + /// /// # Examples /// ``` /// use moq_net::Path; /// - /// let base = Path::new("foo"); - /// let joined = base.join("bar"); - /// assert_eq!(joined.as_str(), "foo/bar"); - /// - /// let joined = base.join(&Path::new("bar")); + /// let base = Path::from_static("foo"); + /// let joined = base.join(&Path::from_static("bar"))?; /// assert_eq!(joined.as_str(), "foo/bar"); + /// # Ok::<(), moq_net::path::TooLong>(()) /// ``` - pub fn join(&self, other: impl AsPath) -> PathOwned { - let other = other.as_path(); - + pub fn join(&self, other: &Path) -> Result { if self.is_empty() { - other.to_owned() + Ok(other.clone()) } else if other.is_empty() { - self.to_owned() + Ok(self.clone()) + } else { + Self::concat(self.as_str(), other.as_str()) + } + } + + // `join` where the result is known to fit, like a prefix and the suffix split from + // one path. Panics otherwise, since that breaks an internal invariant. + pub(crate) fn rejoin(&self, other: &Path) -> Path { + self.join(other).expect("a rejoined path fits") + } + + // `a/b` for two non-empty normalized paths, built in place. + pub(crate) fn concat(a: &str, b: &str) -> Result { + let len = a.len() + 1 + b.len(); + if len > Self::MAX_BYTES { + return Err(TooLong); + } + if len <= INLINE { + let mut buf = [0; INLINE]; + buf[..a.len()].copy_from_slice(a.as_bytes()); + buf[a.len()] = b'/'; + buf[a.len() + 1..len].copy_from_slice(b.as_bytes()); + Ok(Path(Repr::Inline { len: len as u8, buf })) } else { - // Since paths are trimmed, we always need to add a slash - Path(Repr::Shared { - buf: format!("{}/{}", self.as_str(), other.as_str()).into(), + // Sized up front: `format!` guesses low and reallocates. + let mut buf = String::with_capacity(len); + buf.push_str(a); + buf.push('/'); + buf.push_str(b); + Ok(Path(Repr::Shared { + buf: buf.into(), start: 0, - }) + })) } } @@ -345,23 +326,26 @@ impl<'a> Path<'a> { /// A non-empty reference replaces the last segment of the base, matching relative URL /// resolution. `..` segments then pop another segment; other segments are appended. /// Excess `..` is a no-op once the base is empty (subsequent named segments still append). - /// An empty `rel` returns this path as an owned copy. + /// An empty `rel` returns this path unchanged. /// /// [`Relative::new`] strips empty and redundant `.` segments, but preserves a lone `.` /// so it can reference the base's parent. /// + /// Fails if the result is longer than [`Path::MAX_BYTES`]. + /// /// # Examples /// ``` /// use moq_net::{Path, path::Relative}; /// - /// let base = Path::new("a/b/c"); - /// assert_eq!(base.resolve(&Relative::new("./d")).as_str(), "a/b/d"); - /// assert_eq!(base.resolve(&Relative::new(".")).as_str(), "a/b"); - /// assert_eq!(base.resolve(&Relative::new("../d")).as_str(), "a/d"); + /// let base = Path::from_static("a/b/c"); + /// assert_eq!(base.resolve(&Relative::new("./d")?)?.as_str(), "a/b/d"); + /// assert_eq!(base.resolve(&Relative::new(".")?)?.as_str(), "a/b"); + /// assert_eq!(base.resolve(&Relative::new("../d")?)?.as_str(), "a/d"); + /// # Ok::<(), moq_net::path::TooLong>(()) /// ``` - pub fn resolve(&self, rel: &Relative<'_>) -> PathOwned { + pub fn resolve(&self, rel: &Relative) -> Result { if rel.is_empty() { - return self.to_owned(); + return Ok(self.clone()); } let mut segments: Vec<&str> = self.parts().collect(); @@ -377,25 +361,18 @@ impl<'a> Path<'a> { } } - let path = segments.join("/"); - if path.is_empty() { - Path::empty() - } else { - Path(Repr::Shared { - buf: path.into(), - start: 0, - }) - } + Ok(Path(Repr::copy(&segments.join("/"))?)) } - /// Resolve a [`Relative`], returning `None` if it escapes above the root. + /// Resolve a [`Relative`], returning `None` if it escapes above the root or the + /// result is longer than [`Path::MAX_BYTES`]. /// /// Unlike [`Path::resolve`], this distinguishes a valid reference to the empty root /// path from excess `..` segments. Use it when an untrusted relative reference must /// not be clamped to the root. - pub fn try_resolve(&self, rel: &Relative<'_>) -> Option { + pub fn try_resolve(&self, rel: &Relative) -> Option { if rel.is_empty() { - return Some(self.to_owned()); + return Some(self.clone()); } let mut segments: Vec<&str> = self.parts().collect(); @@ -411,15 +388,7 @@ impl<'a> Path<'a> { } } - let path = segments.join("/"); - if path.is_empty() { - Some(Path::empty()) - } else { - Some(Path(Repr::Shared { - buf: path.into(), - start: 0, - })) - } + Repr::copy(&segments.join("/")).ok().map(Path) } /// Express this path relative to `base`: the inverse of [`Path::resolve`]. @@ -434,33 +403,32 @@ impl<'a> Path<'a> { /// /// Returns `None` for a target no reference can name: a path segment may literally be /// `.` or `..`, which resolution reads as navigation instead of as a name. Only the - /// segments past the shared prefix matter, since the rest are never emitted. + /// segments past the shared prefix matter, since the rest are never emitted. A + /// reference longer than [`Path::MAX_BYTES`] can't be named either. /// /// # Examples /// ``` /// use moq_net::Path; /// /// // The base names a broadcast, so its last segment is replaced, not descended into. - /// let base = Path::new("a/b"); - /// assert_eq!(Path::new("a/b/c").relative(&base).unwrap().as_str(), "b/c"); - /// assert_eq!(Path::new("a/c").relative(&base).unwrap().as_str(), "c"); - /// assert_eq!(Path::new("c").relative(&base).unwrap().as_str(), "../c"); + /// let base = Path::from_static("a/b"); + /// assert_eq!(Path::from_static("a/b/c").relative(&base).unwrap().as_str(), "b/c"); + /// assert_eq!(Path::from_static("a/c").relative(&base).unwrap().as_str(), "c"); + /// assert_eq!(Path::from_static("c").relative(&base).unwrap().as_str(), "../c"); /// /// // The lone `.` names the base's parent, which the empty reference cannot. - /// assert_eq!(Path::new("a").relative(&base).unwrap().as_str(), "."); + /// assert_eq!(Path::from_static("a").relative(&base).unwrap().as_str(), "."); /// /// // The base itself. - /// assert_eq!(Path::new("a/b").relative(&base).unwrap().as_str(), ""); + /// assert_eq!(Path::from_static("a/b").relative(&base).unwrap().as_str(), ""); /// /// // A segment named `..` is a legal path component but an unnameable target. - /// assert!(Path::new("a/..").relative(&base).is_none()); + /// assert!(Path::from_static("a/..").relative(&base).is_none()); /// ``` - pub fn relative(&self, base: impl AsPath) -> Option { - let base = base.as_path(); - + pub fn relative(&self, base: &Path) -> Option { // Only the empty reference can name a base whose last segment is itself `.` or `..`, // since resolution replaces that segment rather than emitting it. - if *self == base { + if self == base { return Some(Relative::empty()); } @@ -482,102 +450,169 @@ impl<'a> Path<'a> { if rel.is_empty() { // An empty reference resolves to the base itself, so name the parent explicitly. - return Some(Relative::new(".")); + return Some(Relative(Path::from_static("."))); } - Some(RelativeOwned::from(rel.join("/"))) + Relative::new(&rel.join("/")).ok() + } +} + +// `root/path` for logs, without building the joined path. +#[cfg(feature = "async")] +pub(crate) struct Rooted<'a>(pub &'a Path, pub &'a Path); + +#[cfg(feature = "async")] +impl Display for Rooted<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (self.0.is_empty(), self.1.is_empty()) { + (_, true) => write!(f, "{}", self.0), + (true, false) => write!(f, "{}", self.1), + (false, false) => write!(f, "{}/{}", self.0, self.1), + } } } -// Comparisons, ordering, and hashing all go through `as_str()` so a borrowed and a -// shared path with the same content behave identically (e.g. as map keys). -impl<'b> PartialEq> for Path<'_> { - fn eq(&self, other: &Path<'b>) -> bool { +// An entry point's path argument, failing with the entry point's own error type. +pub(crate) fn convert(path: impl TryInto>) -> Result { + path.try_into().map_err(Into::into) +} + +// The normalized copy when `trimmed` holds consecutive slashes, else `None`. +fn normalize(trimmed: &str) -> Option { + trimmed.contains("//").then(|| { + trimmed + .split('/') + .filter(|s| !s.is_empty()) + .collect::>() + .join("/") + }) +} + +// Comparisons, ordering, and hashing all go through `as_str()` so paths with the same +// content behave identically whatever their representation (e.g. as map keys). +impl PartialEq for Path { + fn eq(&self, other: &Path) -> bool { self.as_str() == other.as_str() } } -impl Eq for Path<'_> {} +impl Eq for Path {} -impl PartialOrd for Path<'_> { +impl PartialOrd for Path { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for Path<'_> { +impl Ord for Path { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_str().cmp(other.as_str()) } } -impl std::hash::Hash for Path<'_> { +impl std::hash::Hash for Path { fn hash(&self, state: &mut H) { self.as_str().hash(state) } } -impl fmt::Debug for Path<'_> { +/// Look up a `Path`-keyed map by `&str` without building a `Path`. The key must be normalized. +impl std::borrow::Borrow for Path { + fn borrow(&self) -> &str { + self.as_str() + } +} + +impl fmt::Debug for Path { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Path").field(&self.as_str()).finish() } } -impl serde::Serialize for Path<'_> { +impl serde::Serialize for Path { fn serialize(&self, serializer: S) -> Result { serializer.serialize_str(self.as_str()) } } -impl<'a> From<&'a str> for Path<'a> { - fn from(s: &'a str) -> Self { +impl TryFrom<&str> for Path { + type Error = TooLong; + + fn try_from(s: &str) -> Result { Self::new(s) } } -impl<'a> From<&'a String> for Path<'a> { - fn from(s: &'a String) -> Self { - // TODO avoid making a copy here +impl TryFrom<&String> for Path { + type Error = TooLong; + + fn try_from(s: &String) -> Result { Self::new(s) } } -impl Default for Path<'_> { +impl From<&Path> for Path { + fn from(p: &Path) -> Self { + p.clone() + } +} + +impl Default for Path { fn default() -> Self { Path::empty() } } -impl From for Path<'_> { - fn from(s: String) -> Self { - Path::new(&s).into_owned() +impl TryFrom for Path { + type Error = TooLong; + + fn try_from(s: String) -> Result { + // `Arc` can't adopt a `String`'s buffer, so this copies like `new`. + Self::new(&s) } } -impl AsRef for Path<'_> { +impl AsRef for Path { fn as_ref(&self) -> &str { self.as_str() } } -impl Display for Path<'_> { +impl Display for Path { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_str()) } } -impl Decode for Path<'_> { +impl From for DecodeError { + fn from(_: TooLong) -> Self { + DecodeError::BoundsExceeded + } +} + +impl From for crate::Error { + fn from(_: TooLong) -> Self { + crate::Error::BoundsExceeded(crate::BoundsExceeded) + } +} + +impl Decode for Path { fn decode(r: &mut Decoder<'_>, _: V) -> Result { - // Borrow the wire bytes until the path is known good, then copy them once. - let path = Path::new(r.str()?); + // Measured before normalization, so redundant slashes can't smuggle a longer string. + let raw = r.str()?; + if raw.len() > Path::MAX_BYTES { + return Err(DecodeError::BoundsExceeded); + } + // Borrowed from the wire bytes, so the path's own copy is the only one. + let path = Path::new(raw)?; if path.parts().count() > Path::MAX_PARTS { return Err(DecodeError::BoundsExceeded); } - Ok(path.into_owned()) + Ok(path) } } -impl Encode for Path<'_> { +impl Encode for Path { fn encode(&self, w: &mut Encoder<'_>, _: V) -> Result<(), EncodeError> { if self.parts().count() > Path::MAX_PARTS { return Err(EncodeError::BoundsExceeded); @@ -618,9 +653,6 @@ impl Decode for Patterns { } } -/// An owned version of [`Relative`] with a `'static` lifetime. -pub type RelativeOwned = Relative<'static>; - /// A relative broadcast path, used to reference one broadcast from another broadcast's content. /// /// Unlike [`Path`] (which is a complete reference within the broadcast namespace), @@ -641,39 +673,47 @@ pub type RelativeOwned = Relative<'static>; /// ``` /// use moq_net::{Path, path::Relative}; /// -/// let rel = Relative::new("./source"); -/// assert_eq!(Path::new("a/b").resolve(&rel).as_str(), "a/source"); +/// let rel = Relative::new("./source")?; +/// assert_eq!(Path::from_static("a/b").resolve(&rel)?.as_str(), "a/source"); /// /// // Redundant `.` segments are stripped on creation. -/// assert_eq!(Relative::new("./a/./b").as_str(), "a/b"); -/// assert_eq!(Relative::new(".").as_str(), "."); +/// assert_eq!(Relative::new("./a/./b")?.as_str(), "a/b"); +/// assert_eq!(Relative::new(".")?.as_str(), "."); +/// # Ok::<(), moq_net::path::TooLong>(()) /// ``` -#[derive(Debug, PartialEq, Eq, Hash, Clone, serde::Serialize)] -pub struct Relative<'a>(Cow<'a, str>); +#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)] +pub struct Relative(Path); -impl<'a> Relative<'a> { +impl serde::Serialize for Relative { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +impl Relative { /// Create a new `Relative` from a string slice. /// /// Leading and trailing slashes are trimmed, consecutive internal slashes collapse to one, /// and redundant `.` segments are stripped. See the type-level doc for the full rules. - pub fn new(s: &'a str) -> Self { + /// Fails if the normalized reference is longer than [`Path::MAX_BYTES`]. + pub fn new(s: &str) -> Result { let trimmed = s.trim_start_matches('/').trim_end_matches('/'); if needs_normalize_relative(trimmed) { - Self(Cow::Owned(normalize_relative_segments(trimmed))) + Ok(Self(Path(Repr::copy(&normalize_relative_segments(trimmed))?))) } else { - Self(Cow::Borrowed(trimmed)) + Ok(Self(Path(Repr::copy(trimmed)?))) } } /// The normalized path as a string slice. pub fn as_str(&self) -> &str { - &self.0 + self.0.as_str() } /// The empty relative path, which resolves to the base path itself. - pub fn empty() -> Relative<'static> { - Relative(Cow::Borrowed("")) + pub const fn empty() -> Relative { + Relative(Path::empty()) } /// True if the path is empty (resolves to the base path itself). @@ -685,46 +725,29 @@ impl<'a> Relative<'a> { pub fn len(&self) -> usize { self.0.len() } - - /// Copy into an owned version with a `'static` lifetime. - pub fn to_owned(&self) -> RelativeOwned { - Relative(Cow::Owned(self.0.to_string())) - } - - /// Convert into an owned version with a `'static` lifetime. - pub fn into_owned(self) -> RelativeOwned { - Relative(Cow::Owned(self.0.into_owned())) - } - - /// Reborrow without copying. - pub fn borrow(&'a self) -> Relative<'a> { - Relative(Cow::Borrowed(&self.0)) - } } -impl<'a> From<&'a str> for Relative<'a> { - fn from(s: &'a str) -> Self { +impl TryFrom<&str> for Relative { + type Error = TooLong; + + fn try_from(s: &str) -> Result { Self::new(s) } } -impl<'a> From<&'a String> for Relative<'a> { - fn from(s: &'a String) -> Self { +impl TryFrom<&String> for Relative { + type Error = TooLong; + + fn try_from(s: &String) -> Result { Self::new(s) } } -impl From for Relative<'_> { - fn from(s: String) -> Self { - let trimmed = s.trim_start_matches('/').trim_end_matches('/'); +impl TryFrom for Relative { + type Error = TooLong; - if needs_normalize_relative(trimmed) { - Self(Cow::Owned(normalize_relative_segments(trimmed))) - } else if trimmed == s { - Self(Cow::Owned(s)) - } else { - Self(Cow::Owned(trimmed.to_string())) - } + fn try_from(s: String) -> Result { + Self::new(&s) } } @@ -746,34 +769,25 @@ fn normalize_relative_segments(trimmed: &str) -> String { } } -impl Default for Relative<'_> { - fn default() -> Self { - Self(Cow::Borrowed("")) - } -} - -impl AsRef for Relative<'_> { +impl AsRef for Relative { fn as_ref(&self) -> &str { - &self.0 + self.as_str() } } -impl Display for Relative<'_> { +impl Display for Relative { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + write!(f, "{}", self.as_str()) } } -// Owned-only deserialization. We use `String::deserialize` so that owned deserializers -// (e.g. `serde_json::from_slice`) work. The borrowed form `<&str>::deserialize` requires -// `'de: 'a`, which is unsatisfiable when `'a = 'static`. -impl<'de> serde::Deserialize<'de> for Relative<'static> { +impl<'de> serde::Deserialize<'de> for Relative { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; - Ok(Relative::from(s)) + Relative::new(&s).map_err(serde::de::Error::custom) } } @@ -783,62 +797,101 @@ mod tests { #[test] fn test_has_prefix() { - let path = Path::new("foo/bar/baz"); + let path = Path::from_static("foo/bar/baz"); // Valid prefixes - test with both &str and &Path - assert!(path.has_prefix("")); - assert!(path.has_prefix("foo")); - assert!(path.has_prefix(Path::new("foo"))); - assert!(path.has_prefix("foo/")); - assert!(path.has_prefix("foo/bar")); - assert!(path.has_prefix(Path::new("foo/bar/"))); - assert!(path.has_prefix("foo/bar/baz")); + assert!(path.has_prefix(&Path::from_static(""))); + assert!(path.has_prefix(&Path::from_static("foo"))); + assert!(path.has_prefix(&Path::from_static("foo"))); + assert!(path.has_prefix(&Path::from_static("foo/"))); + assert!(path.has_prefix(&Path::from_static("foo/bar"))); + assert!(path.has_prefix(&Path::from_static("foo/bar/"))); + assert!(path.has_prefix(&Path::from_static("foo/bar/baz"))); // Invalid prefixes - should not match partial components - assert!(!path.has_prefix("f")); - assert!(!path.has_prefix(Path::new("fo"))); - assert!(!path.has_prefix("foo/b")); - assert!(!path.has_prefix("foo/ba")); - assert!(!path.has_prefix(Path::new("foo/bar/ba"))); + assert!(!path.has_prefix(&Path::from_static("f"))); + assert!(!path.has_prefix(&Path::from_static("fo"))); + assert!(!path.has_prefix(&Path::from_static("foo/b"))); + assert!(!path.has_prefix(&Path::from_static("foo/ba"))); + assert!(!path.has_prefix(&Path::from_static("foo/bar/ba"))); // Edge case: "foobar" should not match "foo" - let path = Path::new("foobar"); - assert!(!path.has_prefix("foo")); - assert!(path.has_prefix(Path::new("foobar"))); + let path = Path::from_static("foobar"); + assert!(!path.has_prefix(&Path::from_static("foo"))); + assert!(path.has_prefix(&Path::from_static("foobar"))); } #[test] fn test_strip_prefix() { - let path = Path::new("foo/bar/baz"); + let path = Path::from_static("foo/bar/baz"); // Test with both &str and &Path - assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz"); - assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz"); - assert_eq!(path.strip_prefix(Path::new("foo/")).unwrap().as_str(), "bar/baz"); - assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz"); - assert_eq!(path.strip_prefix(Path::new("foo/bar/")).unwrap().as_str(), "baz"); - assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), ""); + assert_eq!( + path.strip_prefix(&Path::from_static("")).unwrap().as_str(), + "foo/bar/baz" + ); + assert_eq!( + path.strip_prefix(&Path::from_static("foo")).unwrap().as_str(), + "bar/baz" + ); + assert_eq!( + path.strip_prefix(&Path::from_static("foo/")).unwrap().as_str(), + "bar/baz" + ); + assert_eq!( + path.strip_prefix(&Path::from_static("foo/bar")).unwrap().as_str(), + "baz" + ); + assert_eq!( + path.strip_prefix(&Path::from_static("foo/bar/")).unwrap().as_str(), + "baz" + ); + assert_eq!( + path.strip_prefix(&Path::from_static("foo/bar/baz")).unwrap().as_str(), + "" + ); // Should fail for invalid prefixes - assert!(path.strip_prefix("fo").is_none()); - assert!(path.strip_prefix(Path::new("bar")).is_none()); + assert!(path.strip_prefix(&Path::from_static("fo")).is_none()); + assert!(path.strip_prefix(&Path::from_static("bar")).is_none()); } #[test] fn test_join() { // Test with both &str and &Path - assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar"); - assert_eq!(Path::new("foo/").join(Path::new("bar")).as_str(), "foo/bar"); - assert_eq!(Path::new("").join("bar").as_str(), "bar"); - assert_eq!(Path::new("foo/bar").join(Path::new("baz")).as_str(), "foo/bar/baz"); + assert_eq!( + Path::from_static("foo") + .join(&Path::from_static("bar")) + .unwrap() + .as_str(), + "foo/bar" + ); + assert_eq!( + Path::from_static("foo/") + .join(&Path::from_static("bar")) + .unwrap() + .as_str(), + "foo/bar" + ); + assert_eq!( + Path::from_static("").join(&Path::from_static("bar")).unwrap().as_str(), + "bar" + ); + assert_eq!( + Path::from_static("foo/bar") + .join(&Path::from_static("baz")) + .unwrap() + .as_str(), + "foo/bar/baz" + ); } #[test] fn test_from_conversions() { - let path1 = Path::from("foo/bar"); - let path2 = Path::from("foo/bar"); + let path1 = crate::Path::from_static("foo/bar"); + let path2 = crate::Path::from_static("foo/bar"); let s = String::from("foo/bar"); - let path3 = Path::from(&s); + let path3 = crate::Path::try_from(&s).unwrap(); assert_eq!(path1.as_str(), "foo/bar"); assert_eq!(path2.as_str(), "foo/bar"); @@ -847,33 +900,33 @@ mod tests { #[test] fn test_path_prefix_join() { - let prefix = Path::new("foo"); - let suffix = Path::new("bar/baz"); - let path = prefix.join(&suffix); + let prefix = Path::from_static("foo"); + let suffix = Path::from_static("bar/baz"); + let path = prefix.join(&suffix).unwrap(); assert_eq!(path.as_str(), "foo/bar/baz"); - let prefix = Path::new("foo/"); - let suffix = Path::new("bar/baz"); - let path = prefix.join(&suffix); + let prefix = Path::from_static("foo/"); + let suffix = Path::from_static("bar/baz"); + let path = prefix.join(&suffix).unwrap(); assert_eq!(path.as_str(), "foo/bar/baz"); - let prefix = Path::new("foo"); - let suffix = Path::new("/bar/baz"); - let path = prefix.join(&suffix); + let prefix = Path::from_static("foo"); + let suffix = Path::from_static("/bar/baz"); + let path = prefix.join(&suffix).unwrap(); assert_eq!(path.as_str(), "foo/bar/baz"); - let prefix = Path::new(""); - let suffix = Path::new("bar/baz"); - let path = prefix.join(&suffix); + let prefix = Path::from_static(""); + let suffix = Path::from_static("bar/baz"); + let path = prefix.join(&suffix).unwrap(); assert_eq!(path.as_str(), "bar/baz"); } #[test] fn test_path_prefix_conversions() { - let prefix1 = Path::from("foo/bar"); - let prefix2 = Path::from(String::from("foo/bar")); + let prefix1 = crate::Path::from_static("foo/bar"); + let prefix2 = crate::Path::try_from(String::from("foo/bar")).unwrap(); let s = String::from("foo/bar"); - let prefix3 = Path::from(&s); + let prefix3 = crate::Path::try_from(&s).unwrap(); assert_eq!(prefix1.as_str(), "foo/bar"); assert_eq!(prefix2.as_str(), "foo/bar"); @@ -882,10 +935,10 @@ mod tests { #[test] fn test_path_suffix_conversions() { - let suffix1 = Path::from("foo/bar"); - let suffix2 = Path::from(String::from("foo/bar")); + let suffix1 = crate::Path::from_static("foo/bar"); + let suffix2 = crate::Path::try_from(String::from("foo/bar")).unwrap(); let s = String::from("foo/bar"); - let suffix3 = Path::from(&s); + let suffix3 = crate::Path::try_from(&s).unwrap(); assert_eq!(suffix1.as_str(), "foo/bar"); assert_eq!(suffix2.as_str(), "foo/bar"); @@ -894,21 +947,21 @@ mod tests { #[test] fn test_path_types_basic_operations() { - let prefix = Path::new("foo/bar"); + let prefix = Path::from_static("foo/bar"); assert_eq!(prefix.as_str(), "foo/bar"); assert!(!prefix.is_empty()); assert_eq!(prefix.len(), 7); - let suffix = Path::new("baz/qux"); + let suffix = Path::from_static("baz/qux"); assert_eq!(suffix.as_str(), "baz/qux"); assert!(!suffix.is_empty()); assert_eq!(suffix.len(), 7); - let empty_prefix = Path::new(""); + let empty_prefix = Path::from_static(""); assert!(empty_prefix.is_empty()); assert_eq!(empty_prefix.len(), 0); - let empty_suffix = Path::new(""); + let empty_suffix = Path::from_static(""); assert!(empty_suffix.is_empty()); assert_eq!(empty_suffix.len(), 0); } @@ -916,324 +969,370 @@ mod tests { #[test] fn test_prefix_has_prefix() { // Test empty prefix (should match everything) - let prefix = Path::new("foo/bar"); - assert!(prefix.has_prefix("")); + let prefix = Path::from_static("foo/bar"); + assert!(prefix.has_prefix(&Path::from_static(""))); // Test exact matches - let prefix = Path::new("foo/bar"); - assert!(prefix.has_prefix("foo/bar")); + let prefix = Path::from_static("foo/bar"); + assert!(prefix.has_prefix(&Path::from_static("foo/bar"))); // Test valid prefixes - assert!(prefix.has_prefix("foo")); - assert!(prefix.has_prefix("foo/")); + assert!(prefix.has_prefix(&Path::from_static("foo"))); + assert!(prefix.has_prefix(&Path::from_static("foo/"))); // Test invalid prefixes - partial matches should fail - assert!(!prefix.has_prefix("f")); - assert!(!prefix.has_prefix("fo")); - assert!(!prefix.has_prefix("foo/b")); - assert!(!prefix.has_prefix("foo/ba")); + assert!(!prefix.has_prefix(&Path::from_static("f"))); + assert!(!prefix.has_prefix(&Path::from_static("fo"))); + assert!(!prefix.has_prefix(&Path::from_static("foo/b"))); + assert!(!prefix.has_prefix(&Path::from_static("foo/ba"))); // Test edge cases - let prefix = Path::new("foobar"); - assert!(!prefix.has_prefix("foo")); - assert!(prefix.has_prefix("foobar")); + let prefix = Path::from_static("foobar"); + assert!(!prefix.has_prefix(&Path::from_static("foo"))); + assert!(prefix.has_prefix(&Path::from_static("foobar"))); // Test trailing slash handling - let prefix = Path::new("foo/bar/"); - assert!(prefix.has_prefix("foo")); - assert!(prefix.has_prefix("foo/")); - assert!(prefix.has_prefix("foo/bar")); - assert!(prefix.has_prefix("foo/bar/")); + let prefix = Path::from_static("foo/bar/"); + assert!(prefix.has_prefix(&Path::from_static("foo"))); + assert!(prefix.has_prefix(&Path::from_static("foo/"))); + assert!(prefix.has_prefix(&Path::from_static("foo/bar"))); + assert!(prefix.has_prefix(&Path::from_static("foo/bar/"))); // Test single component - let prefix = Path::new("foo"); - assert!(prefix.has_prefix("")); - assert!(prefix.has_prefix("foo")); - assert!(prefix.has_prefix("foo/")); // "foo/" becomes "foo" after trimming - assert!(!prefix.has_prefix("f")); + let prefix = Path::from_static("foo"); + assert!(prefix.has_prefix(&Path::from_static(""))); + assert!(prefix.has_prefix(&Path::from_static("foo"))); + assert!(prefix.has_prefix(&Path::from_static("foo/"))); // "foo/" becomes "foo" after trimming + assert!(!prefix.has_prefix(&Path::from_static("f"))); // Test empty prefix - let prefix = Path::new(""); - assert!(prefix.has_prefix("")); - assert!(!prefix.has_prefix("foo")); + let prefix = Path::from_static(""); + assert!(prefix.has_prefix(&Path::from_static(""))); + assert!(!prefix.has_prefix(&Path::from_static("foo"))); } #[test] fn test_prefix_join() { // Basic joining - let prefix = Path::new("foo"); - let suffix = Path::new("bar"); - assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); + let prefix = Path::from_static("foo"); + let suffix = Path::from_static("bar"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo/bar"); // Trailing slash on prefix - let prefix = Path::new("foo/"); - let suffix = Path::new("bar"); - assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); + let prefix = Path::from_static("foo/"); + let suffix = Path::from_static("bar"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo/bar"); // Leading slash on suffix - let prefix = Path::new("foo"); - let suffix = Path::new("/bar"); - assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); + let prefix = Path::from_static("foo"); + let suffix = Path::from_static("/bar"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo/bar"); // Trailing slash on suffix - let prefix = Path::new("foo"); - let suffix = Path::new("bar/"); - assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); // trailing slash is trimmed + let prefix = Path::from_static("foo"); + let suffix = Path::from_static("bar/"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo/bar"); // trailing slash is trimmed // Both have slashes - let prefix = Path::new("foo/"); - let suffix = Path::new("/bar"); - assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); + let prefix = Path::from_static("foo/"); + let suffix = Path::from_static("/bar"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo/bar"); // Empty suffix - let prefix = Path::new("foo"); - let suffix = Path::new(""); - assert_eq!(prefix.join(suffix).as_str(), "foo"); + let prefix = Path::from_static("foo"); + let suffix = Path::from_static(""); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo"); // Empty prefix - let prefix = Path::new(""); - let suffix = Path::new("bar"); - assert_eq!(prefix.join(suffix).as_str(), "bar"); + let prefix = Path::from_static(""); + let suffix = Path::from_static("bar"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "bar"); // Both empty - let prefix = Path::new(""); - let suffix = Path::new(""); - assert_eq!(prefix.join(suffix).as_str(), ""); + let prefix = Path::from_static(""); + let suffix = Path::from_static(""); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), ""); // Complex paths - let prefix = Path::new("foo/bar"); - let suffix = Path::new("baz/qux"); - assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); + let prefix = Path::from_static("foo/bar"); + let suffix = Path::from_static("baz/qux"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo/bar/baz/qux"); // Complex paths with slashes - let prefix = Path::new("foo/bar/"); - let suffix = Path::new("/baz/qux/"); - assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed + let prefix = Path::from_static("foo/bar/"); + let suffix = Path::from_static("/baz/qux/"); + assert_eq!(prefix.join(&suffix).unwrap().as_str(), "foo/bar/baz/qux"); // all slashes are trimmed } #[test] fn test_path_ref() { // Test PathRef creation and normalization - let ref1 = Path::new("/foo/bar/"); + let ref1 = Path::from_static("/foo/bar/"); assert_eq!(ref1.as_str(), "foo/bar"); - let ref2 = Path::from("///foo///"); + let ref2 = crate::Path::from_static("///foo///"); assert_eq!(ref2.as_str(), "foo"); // Test PathRef normalizes multiple slashes - let ref3 = Path::new("foo//bar///baz"); + let ref3 = Path::from_static("foo//bar///baz"); assert_eq!(ref3.as_str(), "foo/bar/baz"); // Test conversions - let path = Path::new("foo/bar"); + let path = Path::from_static("foo/bar"); let path_ref = path; assert_eq!(path_ref.as_str(), "foo/bar"); // Test that Path methods work with PathRef - let path2 = Path::new("foo/bar/baz"); + let path2 = Path::from_static("foo/bar/baz"); assert!(path2.has_prefix(&path_ref)); - assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz"); + assert_eq!(path2.strip_prefix(&path_ref).unwrap().as_str(), "baz"); // Test empty PathRef - let empty = Path::new(""); + let empty = Path::from_static(""); assert!(empty.is_empty()); assert_eq!(empty.len(), 0); } #[test] fn test_multiple_consecutive_slashes() { - let path = Path::new("foo//bar///baz"); + let path = Path::from_static("foo//bar///baz"); // Multiple consecutive slashes are collapsed to single slashes assert_eq!(path.as_str(), "foo/bar/baz"); // Test with leading and trailing slashes too - let path2 = Path::new("//foo//bar///baz//"); + let path2 = Path::from_static("//foo//bar///baz//"); assert_eq!(path2.as_str(), "foo/bar/baz"); // Test empty segments are handled correctly - let path3 = Path::new("foo///bar"); + let path3 = Path::from_static("foo///bar"); assert_eq!(path3.as_str(), "foo/bar"); } #[test] fn test_removes_multiple_slashes_comprehensively() { // Test various multiple slash scenarios - assert_eq!(Path::new("foo//bar").as_str(), "foo/bar"); - assert_eq!(Path::new("foo///bar").as_str(), "foo/bar"); - assert_eq!(Path::new("foo////bar").as_str(), "foo/bar"); + assert_eq!(Path::from_static("foo//bar").as_str(), "foo/bar"); + assert_eq!(Path::from_static("foo///bar").as_str(), "foo/bar"); + assert_eq!(Path::from_static("foo////bar").as_str(), "foo/bar"); // Multiple occurrences of double slashes - assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz"); - assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d"); + assert_eq!(Path::from_static("foo//bar//baz").as_str(), "foo/bar/baz"); + assert_eq!(Path::from_static("a//b//c//d").as_str(), "a/b/c/d"); // Mixed slash counts - assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux"); + assert_eq!(Path::from_static("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux"); // With leading and trailing slashes - assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar"); - assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar"); + assert_eq!(Path::from_static("//foo//bar//").as_str(), "foo/bar"); + assert_eq!(Path::from_static("///foo///bar///").as_str(), "foo/bar"); // Edge case: only slashes - assert_eq!(Path::new("//").as_str(), ""); - assert_eq!(Path::new("////").as_str(), ""); + assert_eq!(Path::from_static("//").as_str(), ""); + assert_eq!(Path::from_static("////").as_str(), ""); // Test that operations work correctly with normalized paths - let path_with_slashes = Path::new("foo//bar///baz"); - assert!(path_with_slashes.has_prefix("foo/bar")); - assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz"); - assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux"); - - // Test PathRef to Path conversion - let path_ref = Path::new("foo//bar///baz"); - assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too - let path_from_ref = path_ref.to_owned(); - assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized - } - - #[test] - fn test_path_ref_multiple_slashes() { - // PathRef now normalizes multiple slashes using Cow - let path_ref = Path::new("//foo//bar///baz//"); - assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized - - // Various multiple slash scenarios are normalized in PathRef - assert_eq!(Path::new("foo//bar").as_str(), "foo/bar"); - assert_eq!(Path::new("foo///bar").as_str(), "foo/bar"); - assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d"); - - // Conversion to Path maintains normalized form - assert_eq!(Path::new("foo//bar").to_owned().as_str(), "foo/bar"); - assert_eq!(Path::new("foo///bar").to_owned().as_str(), "foo/bar"); - assert_eq!(Path::new("a//b//c//d").to_owned().as_str(), "a/b/c/d"); - - // Edge cases - assert_eq!(Path::new("//").as_str(), ""); - assert_eq!(Path::new("////").as_str(), ""); - assert_eq!(Path::new("//").to_owned().as_str(), ""); - assert_eq!(Path::new("////").to_owned().as_str(), ""); - - // Test that PathRef avoids allocation when no normalization needed - let normal_path = Path::new("foo/bar/baz"); - assert_eq!(normal_path.as_str(), "foo/bar/baz"); - // This should use Cow::Borrowed internally (no allocation) - - let needs_norm = Path::new("foo//bar"); - assert_eq!(needs_norm.as_str(), "foo/bar"); - // This should use Cow::Owned internally (allocation only when needed) - } - - #[test] - fn test_ergonomic_conversions() { - // Test that all these work ergonomically in function calls - fn takes_path_ref<'a>(p: impl Into>) -> String { - p.into().as_str().to_string() - } - - // Alternative API using the trait alias for better error messages - fn takes_path_ref_with_trait<'a>(p: impl Into>) -> String { - p.into().as_str().to_string() - } - - // String literal - assert_eq!(takes_path_ref("foo//bar"), "foo/bar"); - - // String (owned) - this should now work without & - let owned_string = String::from("foo//bar///baz"); - assert_eq!(takes_path_ref(owned_string), "foo/bar/baz"); - - // &String - let string_ref = String::from("foo//bar"); - assert_eq!(takes_path_ref(string_ref), "foo/bar"); - - // PathRef - let path_ref = Path::new("foo//bar"); - assert_eq!(takes_path_ref(path_ref), "foo/bar"); - - // Path - let path = Path::new("foo//bar"); - assert_eq!(takes_path_ref(path), "foo/bar"); - - // Test that Path::new works with all these types - let _path1 = Path::new("foo/bar"); // &str - let _path2 = Path::new("foo/bar"); // String - should now work - let _path3 = Path::new("foo/bar"); // &String - let _path4 = Path::new("foo/bar"); // PathRef - - // Test the trait alias version works the same - assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar"); - assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar"); + let path_with_slashes = Path::from_static("foo//bar///baz"); + assert!(path_with_slashes.has_prefix(&Path::from_static("foo/bar"))); + assert_eq!( + path_with_slashes + .strip_prefix(&Path::from_static("foo")) + .unwrap() + .as_str(), + "bar/baz" + ); + assert_eq!( + path_with_slashes.join(&Path::from_static("qux")).unwrap().as_str(), + "foo/bar/baz/qux" + ); } #[test] fn test_prefix_strip_prefix() { // Test basic stripping - let prefix = Path::new("foo/bar/baz"); - assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "foo/bar/baz"); - assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar/baz"); - assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar/baz"); - assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "baz"); - assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "baz"); - assert_eq!(prefix.strip_prefix("foo/bar/baz").unwrap().as_str(), ""); + let prefix = Path::from_static("foo/bar/baz"); + assert_eq!( + prefix.strip_prefix(&Path::from_static("")).unwrap().as_str(), + "foo/bar/baz" + ); + assert_eq!( + prefix.strip_prefix(&Path::from_static("foo")).unwrap().as_str(), + "bar/baz" + ); + assert_eq!( + prefix.strip_prefix(&Path::from_static("foo/")).unwrap().as_str(), + "bar/baz" + ); + assert_eq!( + prefix.strip_prefix(&Path::from_static("foo/bar")).unwrap().as_str(), + "baz" + ); + assert_eq!( + prefix.strip_prefix(&Path::from_static("foo/bar/")).unwrap().as_str(), + "baz" + ); + assert_eq!( + prefix.strip_prefix(&Path::from_static("foo/bar/baz")).unwrap().as_str(), + "" + ); // Test invalid prefixes - assert!(prefix.strip_prefix("fo").is_none()); - assert!(prefix.strip_prefix("bar").is_none()); - assert!(prefix.strip_prefix("foo/ba").is_none()); + assert!(prefix.strip_prefix(&Path::from_static("fo")).is_none()); + assert!(prefix.strip_prefix(&Path::from_static("bar")).is_none()); + assert!(prefix.strip_prefix(&Path::from_static("foo/ba")).is_none()); // Test edge cases - let prefix = Path::new("foobar"); - assert!(prefix.strip_prefix("foo").is_none()); - assert_eq!(prefix.strip_prefix("foobar").unwrap().as_str(), ""); + let prefix = Path::from_static("foobar"); + assert!(prefix.strip_prefix(&Path::from_static("foo")).is_none()); + assert_eq!(prefix.strip_prefix(&Path::from_static("foobar")).unwrap().as_str(), ""); // Test empty prefix - let prefix = Path::new(""); - assert_eq!(prefix.strip_prefix("").unwrap().as_str(), ""); - assert!(prefix.strip_prefix("foo").is_none()); + let prefix = Path::from_static(""); + assert_eq!(prefix.strip_prefix(&Path::from_static("")).unwrap().as_str(), ""); + assert!(prefix.strip_prefix(&Path::from_static("foo")).is_none()); // Test single component - let prefix = Path::new("foo"); - assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), ""); - assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming + let prefix = Path::from_static("foo"); + assert_eq!(prefix.strip_prefix(&Path::from_static("foo")).unwrap().as_str(), ""); + assert_eq!(prefix.strip_prefix(&Path::from_static("foo/")).unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming // Test trailing slash handling - let prefix = Path::new("foo/bar/"); - assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar"); - assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar"); - assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), ""); - assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), ""); + let prefix = Path::from_static("foo/bar/"); + assert_eq!(prefix.strip_prefix(&Path::from_static("foo")).unwrap().as_str(), "bar"); + assert_eq!(prefix.strip_prefix(&Path::from_static("foo/")).unwrap().as_str(), "bar"); + assert_eq!(prefix.strip_prefix(&Path::from_static("foo/bar")).unwrap().as_str(), ""); + assert_eq!( + prefix.strip_prefix(&Path::from_static("foo/bar/")).unwrap().as_str(), + "" + ); } - // Pointer-equality checks that owned paths share one allocation through the - // clone / to_owned / strip_prefix flow used by origin announce fan-out. + // A long path shares one allocation through the clone and strip_prefix flow used by + // announce fan-out. A short one is stored inline instead, so its copies share nothing. #[test] - fn test_owned_paths_share_allocation() { - let path = Path::new("customer/room/broadcast").to_owned(); + fn test_long_paths_share_allocation() { + let path = Path::new("customer/room-0123456789/broadcast").unwrap(); - // Cloning an owned path shares the buffer. + // Cloning shares the buffer. let cloned = path.clone(); assert_eq!(path.as_str().as_ptr(), cloned.as_str().as_ptr()); - // as_path + to_owned (how notify queues a path per consumer) shares too. - let requeued = path.as_path().to_owned(); - assert_eq!(path.as_str().as_ptr(), requeued.as_str().as_ptr()); - - // Stripping a prefix from an owned path is offset arithmetic, not a copy. - let stripped = path.strip_prefix("customer").unwrap().to_owned(); - assert_eq!(stripped.as_str(), "room/broadcast"); + // Stripping a prefix leaving a long rest is offset arithmetic, not a copy. + let stripped = path.strip_prefix(&Path::from_static("customer")).unwrap(); + assert_eq!(stripped.as_str(), "room-0123456789/broadcast"); assert_eq!(stripped.as_str().as_ptr(), path.as_str()["customer/".len()..].as_ptr()); - // join produces an owned path whose clones share. - let joined = path.join("alice"); + // A short rest moves inline rather than keeping the long buffer alive. + let short = path + .strip_prefix(&Path::from_static("customer/room-0123456789")) + .unwrap(); + assert_eq!(short.as_str(), "broadcast"); + assert!(matches!(short.0, Repr::Inline { .. })); + + // join produces a path whose clones share. + let joined = path.join(&Path::from_static("alice")).unwrap(); let joined2 = joined.clone(); - assert_eq!(joined.as_str(), "customer/room/broadcast/alice"); + assert_eq!(joined.as_str(), "customer/room-0123456789/broadcast/alice"); assert_eq!(joined.as_str().as_ptr(), joined2.as_str().as_ptr()); } + #[test] + fn test_inline_boundary() { + let fits = "a".repeat(INLINE); + let spills = "a".repeat(INLINE + 1); + assert!(matches!(Path::new(&fits).unwrap().0, Repr::Inline { .. })); + assert!(matches!(Path::new(&spills).unwrap().0, Repr::Shared { .. })); + + // A join exactly at the limit stays inline; one byte more spills. + let half = Path::new(&"b".repeat(INLINE / 2 - 1)).unwrap(); + let joined = half.join(&Path::new(&"c".repeat(INLINE / 2)).unwrap()).unwrap(); + assert_eq!(joined.len(), INLINE); + assert!(matches!(joined.0, Repr::Inline { .. })); + assert_eq!( + joined.as_str(), + format!("{}/{}", "b".repeat(INLINE / 2 - 1), "c".repeat(INLINE / 2)) + ); + assert!(matches!( + joined.join(&Path::from_static("d")).unwrap().0, + Repr::Shared { .. } + )); + + // Multi-byte characters survive the inline copy. + let wide = Path::new("caf\u{e9}/\u{1f3a5}").unwrap(); + assert!(matches!(wide.0, Repr::Inline { .. })); + assert_eq!(wide.as_str(), "caf\u{e9}/\u{1f3a5}"); + assert_eq!( + wide.strip_prefix(&Path::from_static("caf\u{e9}")).unwrap().as_str(), + "\u{1f3a5}" + ); + } + + #[test] + fn test_max_bytes() { + let longest = "a".repeat(Path::MAX_BYTES); + let longer = format!("{longest}b"); + + // Every constructor accepts the limit and refuses one byte more. + assert_eq!(Path::new(&longest).unwrap().len(), Path::MAX_BYTES); + assert_eq!(Path::new(&longer), Err(TooLong)); + assert_eq!(Path::try_from(longer.clone()), Err(TooLong)); + assert_eq!(Relative::new(&longer), Err(TooLong)); + + // The limit applies after normalizing. + let padded = format!( + "/{}//{}/", + "a".repeat(Path::MAX_BYTES / 2), + "b".repeat(Path::MAX_BYTES / 2 - 1) + ); + assert_eq!(Path::new(&padded).unwrap().len(), Path::MAX_BYTES); + + // So do joins and resolution, which can outgrow both sides. + let half = Path::new(&"a".repeat(Path::MAX_BYTES / 2)).unwrap(); + let rest = Path::new(&"b".repeat(Path::MAX_BYTES / 2 - 1)).unwrap(); + assert_eq!(half.join(&rest).unwrap().len(), Path::MAX_BYTES); + assert_eq!(half.join(&Path::new(&format!("{rest}b")).unwrap()), Err(TooLong)); + let rel = Relative::new(&format!("x/{rest}b")).unwrap(); + assert_eq!(Path::new(&format!("{half}/y")).unwrap().resolve(&rel), Err(TooLong)); + assert_eq!(Path::new(&format!("{half}/y")).unwrap().try_resolve(&rel), None); + + // A suffix at the deepest offset still shares the buffer. + let tail = "c".repeat(INLINE + 1); + let head = "a".repeat(Path::MAX_BYTES - tail.len() - 1); + let path = Path::new(&format!("{head}/{tail}")).unwrap(); + let at = path.strip_prefix(&Path::new(&head).unwrap()).unwrap(); + assert_eq!(at.as_str(), tail); + assert_eq!(at.as_str().as_ptr(), path.as_str()[head.len() + 1..].as_ptr()); + } + + #[test] + #[should_panic(expected = "longer than Path::MAX_BYTES")] + fn test_from_static_too_long() { + let literal: &'static str = Box::leak("a".repeat(Path::MAX_BYTES + 1).into_boxed_str()); + Path::from_static(literal); + } + + #[test] + fn test_from_static_borrows() { + const LITERAL: &str = "/static/path/that/is/longer/than/inline/"; + let path = Path::from_static(LITERAL); + assert_eq!(path.as_str(), "static/path/that/is/longer/than/inline"); + assert_eq!(path.as_str().as_ptr(), LITERAL[1..].as_ptr()); + + // A suffix of a static path borrows the literal too. + let rest = path.strip_prefix(&Path::from_static("static")).unwrap(); + assert_eq!(rest.as_str().as_ptr(), LITERAL["/static/".len()..].as_ptr()); + + // Normalizing has to copy. + assert_eq!(Path::from_static("a//b").as_str(), "a/b"); + } + #[test] fn test_parts() { assert_eq!(Path::empty().parts().count(), 0); - assert_eq!(Path::new("foo").parts().collect::>(), ["foo"]); - assert_eq!(Path::new("/foo//bar/").parts().collect::>(), ["foo", "bar"]); + assert_eq!(Path::from_static("foo").parts().collect::>(), ["foo"]); + assert_eq!( + Path::from_static("/foo//bar/").parts().collect::>(), + ["foo", "bar"] + ); } #[test] @@ -1249,10 +1348,11 @@ mod tests { // Encode enforces the limit. let mut buf = Vec::new(); Path::new(&ok) + .unwrap() .encode(&mut Encoder::new(&mut buf, Version::Lite04.into()), Version::Lite04) .unwrap(); assert!(matches!( - Path::new(&too_deep).encode_bytes(Version::Lite04), + Path::new(&too_deep).unwrap().encode_bytes(Version::Lite04), Err(EncodeError::BoundsExceeded) )); @@ -1260,7 +1360,7 @@ mod tests { let decoded = crate::coding::decode_buf(&mut bytes::Bytes::from(buf), Version::Lite04, Path::decode).unwrap(); assert_eq!(decoded.as_str(), ok); - // Decode enforces the limit on a raw string that encode would have refused. + // Decode refuses a raw string longer than the limit. let mut buf = Vec::new(); Encoder::new(&mut buf, Version::Lite04.into()) .string(too_deep.as_str()) @@ -1271,48 +1371,81 @@ mod tests { )); } + #[test] + fn test_wire_max_len() { + use crate::lite::Version; + + let longest = "a".repeat(Path::MAX_BYTES); + let longer = format!("{longest}b"); + + // Decode round-trips at the limit. + let mut buf = Path::new(&longest).unwrap().encode_bytes(Version::Lite04).unwrap(); + let decoded = crate::coding::decode_buf(&mut buf, Version::Lite04, Path::decode).unwrap(); + assert_eq!(decoded.as_str(), longest); + + // A message carrying the longest path still fits the lite message limit. + let tracks = crate::lite::Tracks { + broadcast: Path::new(&longest).unwrap(), + epoch: None, + }; + let buf = tracks.encode_bytes(Version::Lite07).unwrap(); + let (decoded, _) = crate::lite::Tracks::decode_slice(&buf, Version::Lite07).unwrap(); + assert_eq!(decoded.broadcast.as_str(), longest); + + // Decode refuses a raw string longer than the limit, even one that normalizes shorter. + for raw in [longer.clone(), format!("{}a", "/".repeat(Path::MAX_BYTES))] { + let mut buf = Vec::new(); + Encoder::new(&mut buf, Version::Lite04.into()).string(&raw).unwrap(); + assert!(matches!( + crate::coding::decode_buf(&mut bytes::Bytes::from(buf), Version::Lite04, Path::decode), + Err(DecodeError::BoundsExceeded) + )); + } + } + #[test] fn test_owned_empty_paths() { // Empty paths never allocate and stay well-behaved. - let empty = Path::new("").to_owned(); + let empty = Path::from_static(""); assert!(empty.is_empty()); assert_eq!(empty, Path::empty()); - let path = Path::new("foo").to_owned(); - let rest = path.strip_prefix("foo").unwrap().to_owned(); + let path = Path::from_static("foo"); + let rest = path.strip_prefix(&Path::from_static("foo")).unwrap(); assert!(rest.is_empty()); } #[test] fn test_path_relative_normalize() { - assert_eq!(Relative::new("foo").as_str(), "foo"); - assert_eq!(Relative::new("/foo/").as_str(), "foo"); - assert_eq!(Relative::new("foo//bar").as_str(), "foo/bar"); - assert_eq!(Relative::new("../foo").as_str(), "../foo"); - assert_eq!(Relative::new("../../a/b").as_str(), "../../a/b"); - assert!(Relative::new("").is_empty()); + assert_eq!(Relative::new("foo").unwrap().as_str(), "foo"); + assert_eq!(Relative::new("/foo/").unwrap().as_str(), "foo"); + assert_eq!(Relative::new("foo//bar").unwrap().as_str(), "foo/bar"); + assert_eq!(Relative::new("../foo").unwrap().as_str(), "../foo"); + assert_eq!(Relative::new("../../a/b").unwrap().as_str(), "../../a/b"); + assert!(Relative::new("").unwrap().is_empty()); } #[test] fn test_path_relative_normalizes_dot_segments() { - assert_eq!(Relative::new(".").as_str(), "."); - assert_eq!(Relative::new("././").as_str(), "."); - assert_eq!(Relative::new("./foo").as_str(), "foo"); - assert_eq!(Relative::new("foo/./bar").as_str(), "foo/bar"); - assert_eq!(Relative::new("./../foo").as_str(), "../foo"); - // From takes the same normalization. - assert_eq!(Relative::from("./foo".to_string()).as_str(), "foo"); - assert_eq!(Relative::from(".".to_string()).as_str(), "."); + assert_eq!(Relative::new(".").unwrap().as_str(), "."); + assert_eq!(Relative::new("././").unwrap().as_str(), "."); + assert_eq!(Relative::new("./foo").unwrap().as_str(), "foo"); + assert_eq!(Relative::new("foo/./bar").unwrap().as_str(), "foo/bar"); + assert_eq!(Relative::new("./../foo").unwrap().as_str(), "../foo"); + // TryFrom takes the same normalization. + assert_eq!(Relative::try_from("./foo".to_string()).unwrap().as_str(), "foo"); + assert_eq!(Relative::try_from(".".to_string()).unwrap().as_str(), "."); } #[test] fn test_resolve_replaces_base_name() { - let base = Path::new("a/b"); - assert_eq!(base.resolve(&Relative::new("c")).as_str(), "a/c"); - assert_eq!(base.resolve(&Relative::new("c/d")).as_str(), "a/c/d"); + let base = Path::from_static("a/b"); + assert_eq!(base.resolve(&Relative::new("c").unwrap()).unwrap().as_str(), "a/c"); + assert_eq!(base.resolve(&Relative::new("c/d").unwrap()).unwrap().as_str(), "a/c/d"); assert_eq!( - Path::new("foo.hang/catalog.pro") - .resolve(&Relative::new("./transcode.pro")) + Path::from_static("foo.hang/catalog.pro") + .resolve(&Relative::new("./transcode.pro").unwrap()) + .unwrap() .as_str(), "foo.hang/transcode.pro" ); @@ -1320,69 +1453,75 @@ mod tests { #[test] fn test_resolve_empty_rel_returns_base() { - let base = Path::new("a/b"); - assert_eq!(base.resolve(&Relative::new("")).as_str(), "a/b"); + let base = Path::from_static("a/b"); + assert_eq!(base.resolve(&Relative::new("").unwrap()).unwrap().as_str(), "a/b"); } #[test] fn test_resolve_single_dotdot() { - let base = Path::new("a/b/c"); - assert_eq!(base.resolve(&Relative::new("../d")).as_str(), "a/d"); - assert_eq!(base.resolve(&Relative::new("..")).as_str(), "a"); + let base = Path::from_static("a/b/c"); + assert_eq!(base.resolve(&Relative::new("../d").unwrap()).unwrap().as_str(), "a/d"); + assert_eq!(base.resolve(&Relative::new("..").unwrap()).unwrap().as_str(), "a"); } #[test] fn test_resolve_multiple_dotdot() { - let base = Path::new("a/b/c"); - assert_eq!(base.resolve(&Relative::new("../../x")).as_str(), "x"); - assert_eq!(base.resolve(&Relative::new("../../../x")).as_str(), "x"); + let base = Path::from_static("a/b/c"); + assert_eq!(base.resolve(&Relative::new("../../x").unwrap()).unwrap().as_str(), "x"); + assert_eq!( + base.resolve(&Relative::new("../../../x").unwrap()).unwrap().as_str(), + "x" + ); } #[test] fn test_resolve_dotdot_clamps_at_root() { - let base = Path::new("a"); + let base = Path::from_static("a"); // Excess `..` clamps at the root instead of escaping it. - assert_eq!(base.resolve(&Relative::new("../../../foo")).as_str(), "foo"); - assert_eq!(base.resolve(&Relative::new("..")).as_str(), ""); + assert_eq!( + base.resolve(&Relative::new("../../../foo").unwrap()).unwrap().as_str(), + "foo" + ); + assert_eq!(base.resolve(&Relative::new("..").unwrap()).unwrap().as_str(), ""); } #[test] fn test_resolve_empty_base() { let base = Path::empty(); - assert_eq!(base.resolve(&Relative::new("foo")).as_str(), "foo"); - assert_eq!(base.resolve(&Relative::new("..")).as_str(), ""); + assert_eq!(base.resolve(&Relative::new("foo").unwrap()).unwrap().as_str(), "foo"); + assert_eq!(base.resolve(&Relative::new("..").unwrap()).unwrap().as_str(), ""); } #[test] fn test_resolve_dot_names_parent() { - let base = Path::new("a/b"); - assert_eq!(base.resolve(&Relative::new(".")).as_str(), "a"); - assert_eq!(base.resolve(&Relative::new("./c")).as_str(), "a/c"); - assert_eq!(base.resolve(&Relative::new("./../c")).as_str(), "c"); + let base = Path::from_static("a/b"); + assert_eq!(base.resolve(&Relative::new(".").unwrap()).unwrap().as_str(), "a"); + assert_eq!(base.resolve(&Relative::new("./c").unwrap()).unwrap().as_str(), "a/c"); + assert_eq!(base.resolve(&Relative::new("./../c").unwrap()).unwrap().as_str(), "c"); } #[test] fn test_resolve_self_reference_via_sibling_name() { // Naming the base within its parent yields the base unchanged, which lets the // caller compare resolved == base to detect a self-reference. - let base = Path::new("a/b"); - assert_eq!(base.resolve(&Relative::new("./b")).as_str(), "a/b"); + let base = Path::from_static("a/b"); + assert_eq!(base.resolve(&Relative::new("./b").unwrap()).unwrap().as_str(), "a/b"); } #[test] fn test_try_resolve_distinguishes_root_from_escape() { - let base = Path::new("top"); - assert_eq!(base.try_resolve(&Relative::new(".")).unwrap().as_str(), ""); - assert!(base.try_resolve(&Relative::new("..")).is_none()); + let base = Path::from_static("top"); + assert_eq!(base.try_resolve(&Relative::new(".").unwrap()).unwrap().as_str(), ""); + assert!(base.try_resolve(&Relative::new("..").unwrap()).is_none()); - let nested = Path::new("a/b"); - assert_eq!(nested.try_resolve(&Relative::new("..")).unwrap().as_str(), ""); - assert!(nested.try_resolve(&Relative::new("../..")).is_none()); + let nested = Path::from_static("a/b"); + assert_eq!(nested.try_resolve(&Relative::new("..").unwrap()).unwrap().as_str(), ""); + assert!(nested.try_resolve(&Relative::new("../..").unwrap()).is_none()); } #[test] fn test_relative() { - let rel = |target: &str, base: &str| Path::new(target).relative(base).unwrap(); + let rel = |target: &str, base: &str| Path::new(target).unwrap().relative(&Path::new(base).unwrap()).unwrap(); // Nested under the base: the base's own last segment is replaced, so it repeats. assert_eq!(rel("foo/bar/baz", "foo/bar").as_str(), "bar/baz"); @@ -1407,17 +1546,28 @@ mod tests { fn test_relative_rejects_unnameable_targets() { // A segment literally named `.` or `..` is a legal path component, but resolution // would walk on it instead of naming it. - assert!(Path::new("a/../b").relative("").is_none()); - assert!(Path::new("x/./y").relative("x/z").is_none()); - assert!(Path::new("a/..").relative("a/b").is_none()); + assert!(Path::from_static("a/../b").relative(&Path::from_static("")).is_none()); + assert!(Path::from_static("x/./y").relative(&Path::from_static("x/z")).is_none()); + assert!(Path::from_static("a/..").relative(&Path::from_static("a/b")).is_none()); // A base is always nameable by itself, however its last segment is spelled. - assert_eq!(Path::new("a/..").relative("a/..").unwrap().as_str(), ""); + assert_eq!( + Path::from_static("a/..") + .relative(&Path::from_static("a/..")) + .unwrap() + .as_str(), + "" + ); // Dot segments inside the shared prefix are never emitted, so they are fine. - let rel = Path::new("a/../b/x").relative("a/../b/c").unwrap(); + let rel = Path::from_static("a/../b/x") + .relative(&Path::from_static("a/../b/c")) + .unwrap(); assert_eq!(rel.as_str(), "x"); - assert_eq!(Path::new("a/../b/c").resolve(&rel).as_str(), "a/../b/x"); + assert_eq!( + Path::from_static("a/../b/c").resolve(&rel).unwrap().as_str(), + "a/../b/x" + ); } #[test] @@ -1428,8 +1578,8 @@ mod tests { for base in paths { for target in paths { - let base = Path::new(base); - let target = Path::new(target); + let base = Path::new(base).unwrap(); + let target = Path::new(target).unwrap(); let Some(rel) = target.relative(&base) else { // Only an unnameable target may be refused, and never the base itself. assert!( @@ -1439,7 +1589,7 @@ mod tests { continue; }; - assert_eq!(base.resolve(&rel), target, "{base} -> {target} via {rel}"); + assert_eq!(base.resolve(&rel), Ok(target.clone()), "{base} -> {target} via {rel}"); // The reference is derived from a real target, so it never escapes the root. assert!( base.try_resolve(&rel).is_some(), diff --git a/rs/moq-net/src/server.rs b/rs/moq-net/src/server.rs index de0b6310dcb..8dc3c2f61f2 100644 --- a/rs/moq-net/src/server.rs +++ b/rs/moq-net/src/server.rs @@ -1381,7 +1381,11 @@ mod tests { .accept_request(start, session.clone()) .await .unwrap() - .with_stats(registry.tier(Default::default()).session("room")) + .with_stats( + registry + .tier(Default::default()) + .session(crate::Path::from_static("room")), + ) .with_deadline(deadline); let Err(err) = request.ok().await else { panic!("a reply the peer never took completed"); @@ -1409,7 +1413,11 @@ mod tests { .accept_request(moq_clock::now(), session.clone()) .await .unwrap() - .with_stats(registry.tier(Default::default()).session("room")); + .with_stats( + registry + .tier(Default::default()) + .session(crate::Path::from_static("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!( @@ -1435,7 +1443,11 @@ mod tests { .accept_request(moq_clock::now(), session) .await .unwrap() - .with_stats(registry.tier(Default::default()).session("room")) + .with_stats( + registry + .tier(Default::default()) + .session(crate::Path::from_static("room")), + ) .close(err.clone()); let failures = registry.snapshot().sessions()[0].1.sessions_failed; for kind in stats::Failure::ALL { diff --git a/rs/moq-net/src/stats.rs b/rs/moq-net/src/stats.rs index 7c52cb37a0a..2588276415c 100644 --- a/rs/moq-net/src/stats.rs +++ b/rs/moq-net/src/stats.rs @@ -144,7 +144,7 @@ use std::{ use kio::Lock; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{AsPath, PathOwned, Pattern, Patterns, util::SendCell}; +use crate::{Path, Pattern, Patterns, util::SendCell}; mod outcomes; pub use outcomes::{Failure, Failures, RefusalKind, Refusals}; @@ -815,16 +815,16 @@ impl Presence { /// `Tier::new("region/sjc")` records under `"region/sjc"`. The label is an /// arbitrary path chosen by business logic; an empty label is the default tier. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] -pub struct Tier(PathOwned); +pub struct Tier(Path); impl Tier { /// A tier with the given label. An empty label is the default tier. - pub fn new(label: impl Into) -> Self { + pub fn new(label: impl Into) -> Self { Self(label.into()) } /// The tier label, empty for the default tier. - pub fn label(&self) -> &PathOwned { + pub fn label(&self) -> &Path { &self.0 } @@ -1017,7 +1017,7 @@ pub struct Report { #[non_exhaustive] pub struct TrafficEntry { /// The broadcast path the counters are keyed by. - pub path: PathOwned, + pub path: Path, /// The tier the counters recorded under. pub tier: Tier, /// Egress counters (this node publishing to peers). @@ -1036,7 +1036,7 @@ pub struct SessionEntry { /// The tier the sessions recorded under. pub tier: Tier, /// The label the sessions record under, empty for sessions without one. - pub label: PathOwned, + pub label: Path, /// Cumulative request admissions and connect/disconnect gauge. pub presence: Presence, /// This is the label's final readout: its last session or request ended, so the @@ -1091,12 +1091,12 @@ struct Shared { /// Completed entries folded by tier before pruning. Lock before either map /// so a snapshot sees each entry either here or live, never both or neither. retired: Lock, - entries: Lock>>, + entries: Lock>>, /// Connected-session gauges keyed by `(tier, label)`. Independent of any /// broadcast; surfaced on the per-tier session tracks and in the totals and /// presence tracks. A tier's inner map is /// created the first time a session records under it. - sessions: Lock>>>, + sessions: Lock>>>, wire: Lock>>, } @@ -1185,7 +1185,7 @@ impl Registry { /// unless a guard still holds it. #[cfg(any(test, feature = "fuzz"))] #[cfg(feature = "async")] - pub(crate) fn bump_publisher_bytes(&self, path: impl AsPath, tier: &Tier, n: u64) { + pub(crate) fn bump_publisher_bytes(&self, path: impl Into, tier: &Tier, n: u64) { let Some(entry) = self.entry(path) else { return; }; @@ -1195,7 +1195,7 @@ impl Registry { /// Resolve the pre-label payload meter used as the stats write benchmark's baseline. #[cfg(any(test, feature = "fuzz"))] #[cfg(feature = "async")] - pub(crate) fn publisher_meter(&self, path: impl AsPath, tier: &Tier) -> Meter { + pub(crate) fn publisher_meter(&self, path: impl Into, tier: &Tier) -> Meter { Meter { counters: self.entry(path).map(|entry| entry.tier(tier)), traffic: None, @@ -1203,21 +1203,20 @@ impl Registry { } } - fn entry(&self, path: impl AsPath) -> Option> { + fn entry(&self, path: impl Into) -> Option> { // A disabled registry never allocates state. let shared = self.shared.as_ref()?; - let path = path.as_path(); + let path: Path = path.into(); // Skip excluded paths (our own stats broadcasts and any sibling category // under the same prefix) so serving a stats broadcast doesn't generate // more stats. if self.exclude.matches(path.as_str()) { return None; } - let owned = path.to_owned(); let mut entries = shared.entries.lock(); Some( entries - .entry(owned) + .entry(path) .or_insert_with(|| Arc::new(BroadcastEntry::new())) .clone(), ) @@ -1226,9 +1225,9 @@ impl Registry { /// Get-or-create the session gauge for `label` on `tier`. `None` for a /// disabled registry. Unlike [`Self::entry`], labels key sessions rather than /// broadcasts, so no cycle-breaking filter is needed. - fn session_counters(&self, tier: &Tier, label: impl AsPath) -> Option> { + fn session_counters(&self, tier: &Tier, label: impl Into) -> Option> { let shared = self.shared.as_ref()?; - let owned = label.as_path().to_owned(); + let owned = label.into(); let mut sessions = shared.sessions.lock(); Some( sessions @@ -1375,7 +1374,7 @@ pub struct Handle { impl Handle { /// Count a session refused outside relay admission under its resolved label and tier. - pub fn refuse(&self, label: impl AsPath, reason: RefusalKind) { + pub fn refuse(&self, label: impl Into, reason: RefusalKind) { if let Some(presence) = self.stats.session_counters(&self.tier, label) { presence .sessions_refused @@ -1390,19 +1389,19 @@ impl Handle { /// session that merely connects is still billable. Surfaced on the session /// track for this tier, keyed by `label`, in its totals, and in `label`'s /// presence track. - pub fn session(&self, label: impl AsPath) -> Session { + pub fn session(&self, label: impl Into) -> Session { self.context(label, Context::Session) } /// Count one admitted request under `label` and meter its traffic without session presence. /// /// A request has no session end to classify, so it never records a failure. - pub fn request(&self, label: impl AsPath) -> Session { + pub fn request(&self, label: impl Into) -> Session { self.context(label, Context::Request) } - fn context(&self, label: impl AsPath, context: Context) -> Session { - let label = label.as_path().to_owned(); + fn context(&self, label: impl Into, context: Context) -> Session { + let label = label.into(); let presence = self.stats.session_counters(&self.tier, &label); if let Some(presence) = &presence { match context { @@ -1581,7 +1580,7 @@ struct SessionInner { /// Egress viewer refcount, keyed by absolute broadcast path: the first active /// subscription this context opens for a broadcast bumps `broadcasts_started`, the last /// to close bumps `broadcasts_ended` on the same counters, even across a tier change. - viewers: Mutex>, + viewers: Mutex>, /// What this session holds against each cap right now, so a tier change can seed the /// new tier's peaks with slots it still holds. held: Mutex)>>, @@ -1593,7 +1592,7 @@ struct SessionInner { struct Current { failure: Option, tier: Tier, - label: PathOwned, + label: Path, /// The presence gauge for `(tier, label)`, or `None` for a disabled registry. presence: Option>, } @@ -1667,9 +1666,9 @@ impl Session { } /// Record later traffic and session presence under `(tier, label)`; a request stays counted where admitted. - pub fn retag(&self, tier: Tier, label: impl AsPath) { + pub fn retag(&self, tier: Tier, label: impl Into) { let Some(inner) = &self.inner else { return }; - let label = label.as_path().to_owned(); + let label = label.into(); let mut current = inner.current.lock().unwrap_or_else(PoisonError::into_inner); if current.tier == tier && current.label == label { return; @@ -1740,20 +1739,20 @@ impl Session { /// Egress (publisher / reads) scope for a broadcast path. The path is the /// absolute broadcast name. - pub(crate) fn egress(&self, path: impl AsPath) -> Scope { + pub(crate) fn egress(&self, path: impl Into) -> Scope { self.scope(path, Side::Publisher) } /// Ingress (subscriber / writes) scope for a broadcast path. - pub(crate) fn ingress(&self, path: impl AsPath) -> Scope { + pub(crate) fn ingress(&self, path: impl Into) -> Scope { self.scope(path, Side::Subscriber) } - fn scope(&self, path: impl AsPath, side: Side) -> Scope { + fn scope(&self, path: impl Into, side: Side) -> Scope { let Some(inner) = &self.inner else { return Scope::default(); }; - let path = path.as_path().to_owned(); + let path = path.into(); let resolved = inner.resolve(&path); Scope { session: self.clone(), @@ -1765,7 +1764,7 @@ impl Session { /// Register one active egress subscription to `path` recording on `counters`. /// The first bumps `broadcasts_started` there. - fn viewer_open(&self, path: &PathOwned, counters: &Arc) { + fn viewer_open(&self, path: &Path, counters: &Arc) { let Some(inner) = &self.inner else { return }; let mut viewers = inner.viewers.lock().unwrap_or_else(PoisonError::into_inner); let viewer = viewers.entry(path.clone()).or_insert_with(|| { @@ -1780,7 +1779,7 @@ impl Session { /// Release one active egress subscription to `path`. The last bumps /// `broadcasts_ended` on the counters the first opened on. - fn viewer_close(&self, path: &PathOwned) { + fn viewer_close(&self, path: &Path) { let Some(inner) = &self.inner else { return }; let mut viewers = inner.viewers.lock().unwrap_or_else(PoisonError::into_inner); let Some(viewer) = viewers.get_mut(path) else { return }; @@ -1801,7 +1800,7 @@ impl Session { impl SessionInner { /// The counters `path` records on under the current tier, tagged with the /// generation they were resolved at. - fn resolve(&self, path: &PathOwned) -> Resolved { + fn resolve(&self, path: &Path) -> Resolved { let (generation, tier) = { let current = self.current.lock().unwrap_or_else(PoisonError::into_inner); (self.generation.load(Ordering::Relaxed), current.tier.clone()) @@ -1945,7 +1944,7 @@ pub(crate) struct Scope { side: Side, /// Absolute broadcast path, used to key the viewer refcount and as the /// `announced_bytes` length. - path: PathOwned, + path: Path, } /// A [`Scope`]'s counters and the session tier generation they belong to. @@ -2058,7 +2057,7 @@ pub(crate) struct Subscription { counters: Option>, side: Side, /// `Some((session, path))` on the egress side, to release the viewer refcount. - viewer: Option<(Session, PathOwned)>, + viewer: Option<(Session, Path)>, } impl Drop for Subscription { @@ -2111,9 +2110,9 @@ mod tests { #[test] fn wire_deltas_follow_label_and_tier_changes() { let registry = test_stats(); - let old = Tier::new("old"); - let new = Tier::new("new"); - let session = registry.tier(old.clone()).session("room"); + let old = Tier::new(crate::Path::from_static("old")); + let new = Tier::new(crate::Path::from_static("new")); + let session = registry.tier(old.clone()).session(crate::Path::from_static("room")); let bytes = counters(10, 20); let _wire = session.wire(bytes.clone()); let report = drain(®istry); @@ -2126,7 +2125,7 @@ mod tests { ); bytes.set_sent_bytes(15); bytes.set_received_bytes(27); - session.retag(new.clone(), "hall"); + session.retag(new.clone(), crate::Path::from_static("hall")); bytes.set_sent_bytes(25); bytes.set_received_bytes(50); let report = drain(®istry); @@ -2143,7 +2142,7 @@ mod tests { #[test] fn a_wire_counter_reset_never_lowers_presence() { let registry = test_stats(); - let session = registry.tier(Tier::default()).session("room"); + let session = registry.tier(Tier::default()).session(crate::Path::from_static("room")); let bytes = crate::transport::Wire::new(); let _wire = session.wire(bytes.clone()); for (reading, expected) in [((20, 30), (20, 30)), ((3, 4), (20, 30)), ((8, 10), (25, 36))] { @@ -2160,7 +2159,7 @@ mod tests { #[test] fn disabled_wire_collection_reports_nothing() { let registry = Registry::disabled(); - let session = registry.tier(Tier::default()).session("root"); + let session = registry.tier(Tier::default()).session(crate::Path::from_static("root")); let wire = session.wire(counters(5, 7)); assert!(drain(®istry).sessions.is_empty()); drop(wire); @@ -2170,7 +2169,7 @@ mod tests { #[test] fn wire_guards_hold_the_session_until_the_final_reading() { let registry = test_stats(); - let session = registry.tier(Tier::default()).session("room"); + let session = registry.tier(Tier::default()).session(crate::Path::from_static("room")); let wire = session.wire(counters(19, 23)); drop(session); assert_eq!(drain(®istry).sessions[0].presence.active(), 1); @@ -2195,11 +2194,14 @@ mod tests { #[test] fn outcomes_survive_clones_tier_changes_and_pruning() { let stats = test_stats(); - let handle = stats.tier(Tier::new("gateway")); - handle.refuse("root", RefusalKind::Forbidden); - let session = handle.session("root"); + let handle = stats.tier(Tier::new(crate::Path::from_static("gateway"))); + handle.refuse(crate::Path::from_static("root"), RefusalKind::Forbidden); + let session = handle.session(crate::Path::from_static("root")); let clone = session.clone(); - session.retag(Tier::new("next"), "root"); + session.retag( + Tier::new(crate::Path::from_static("next")), + crate::Path::from_static("root"), + ); // A reconnect's later end replaces an earlier one. clone.end(&crate::Error::Transport("lost".into())); session.end(&crate::Error::IdleTimeout); @@ -2227,10 +2229,12 @@ mod tests { let after = stats.snapshot(); assert_eq!(before.sessions(), after.sessions(), "retirement keeps outcomes once"); let disabled = Registry::disabled(); - disabled.tier(Tier::default()).refuse("root", RefusalKind::Refused); disabled .tier(Tier::default()) - .session("root") + .refuse(crate::Path::from_static("root"), RefusalKind::Refused); + disabled + .tier(Tier::default()) + .session(crate::Path::from_static("root")) .end(&crate::Error::Expired); assert!(disabled.snapshot().sessions().is_empty()); } @@ -2267,19 +2271,18 @@ mod tests { .shared() .entries .lock() - .get(&PathOwned::from(path.to_string())) + .get(&crate::Path::try_from(path.to_string()).unwrap()) .expect("entry") .tier(tier) } /// The [`Presence`] for `(tier, label)`, or `None` if absent. fn session_snapshot(stats: &Registry, tier: &Tier, label: &str) -> Option { - stats - .shared() - .sessions - .lock() - .get(tier) - .and_then(|labels| labels.get(&PathOwned::from(label.to_string())).map(|c| c.snapshot().0)) + stats.shared().sessions.lock().get(tier).and_then(|labels| { + labels + .get(&crate::Path::try_from(label.to_string()).unwrap()) + .map(|c| c.snapshot().0) + }) } fn test_stats() -> Registry { @@ -2289,14 +2292,16 @@ mod tests { #[test] fn default_and_named_tiers_are_independent() { let stats = test_stats(); - let default = stats.tier(Tier::default()).session("root"); - let regional = stats.tier(Tier::new("region/sjc")).session("root"); + let default = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let regional = stats + .tier(Tier::new(crate::Path::from_static("region/sjc"))) + .session(crate::Path::from_static("root")); - default.egress("demo/bbb").meter().bytes(100); - regional.ingress("demo/bbb").meter().bytes(7); + default.egress(crate::Path::from_static("demo/bbb")).meter().bytes(100); + regional.ingress(crate::Path::from_static("demo/bbb")).meter().bytes(7); let default_counters = tier_counters(&stats, "demo/bbb", &Tier::default()); - let regional_counters = tier_counters(&stats, "demo/bbb", &Tier::new("region/sjc")); + let regional_counters = tier_counters(&stats, "demo/bbb", &Tier::new(crate::Path::from_static("region/sjc"))); assert_eq!(default_counters.publisher.bytes.load(Relaxed), 100); assert_eq!(default_counters.subscriber.bytes.load(Relaxed), 0); assert_eq!(regional_counters.publisher.bytes.load(Relaxed), 0); @@ -2312,7 +2317,10 @@ mod tests { let stats = test_stats(); let tier = stats.tier(Tier::default()); - let (s1, s2) = (tier.session("acme"), tier.session("acme")); + let (s1, s2) = ( + tier.session(crate::Path::from_static("acme")), + tier.session(crate::Path::from_static("acme")), + ); let announces = Slots::new(10).with_stats(&s1, Cap::Announces); let subscriptions = Slots::new(10).with_stats(&s2, Cap::Subscriptions); let other = Slots::new(10).with_stats(&s2, Cap::Announces); @@ -2344,13 +2352,13 @@ mod tests { use crate::session::Slots; let stats = test_stats(); - let session = stats.tier(Tier::default()).session("acme"); + let session = stats.tier(Tier::default()).session(crate::Path::from_static("acme")); let subscriptions = Slots::new(10).with_stats(&session, Cap::Subscriptions); let mut held: Vec<_> = (0..5).map(|_| subscriptions.acquire().unwrap()).collect(); held.truncate(3); - let gold = Tier::new("gold"); - session.retag(gold.clone(), "acme"); + let gold = Tier::new(crate::Path::from_static("gold")); + session.retag(gold.clone(), crate::Path::from_static("acme")); let peak = |tier: &Tier| { let (_, presence) = stats @@ -2369,23 +2377,23 @@ mod tests { fn snapshot_rolls_up_by_tier_role_and_sessions() { let stats = test_stats(); let default = stats.tier(Tier::default()); - let regional = stats.tier(Tier::new("region/sjc")); + let regional = stats.tier(Tier::new(crate::Path::from_static("region/sjc"))); // Two default-tier sessions under one label, one regional; presence sums them. - let s1 = default.session("acme"); - let _s2 = default.session("acme"); - let s3 = regional.session("peer"); + let s1 = default.session(crate::Path::from_static("acme")); + let _s2 = default.session(crate::Path::from_static("acme")); + let s3 = regional.session(crate::Path::from_static("peer")); // Default-tier egress across two broadcasts; the snapshot sums them. { - let m = s1.egress("demo/aaa").meter(); + let m = s1.egress(crate::Path::from_static("demo/aaa")).meter(); m.bytes(100); m.frames(1); m.group(); } - s1.egress("demo/bbb").meter().bytes(50); + s1.egress(crate::Path::from_static("demo/bbb")).meter().bytes(50); // Regional ingress on a different tier/role stays isolated. - s3.ingress("demo/aaa").meter().bytes(7); + s3.ingress(crate::Path::from_static("demo/aaa")).meter().bytes(7); let snap = stats.snapshot(); @@ -2405,10 +2413,13 @@ mod tests { assert_eq!(default_publisher.frames, 1); assert_eq!(default_publisher.groups, 1); - let regional_subscriber = slot(Tier::new("region/sjc"), Role::Subscriber); + let regional_subscriber = slot(Tier::new(crate::Path::from_static("region/sjc")), Role::Subscriber); assert_eq!(regional_subscriber.bytes, 7, "regional ingress isolated by tier/role"); assert_eq!(slot(Tier::default(), Role::Subscriber).bytes, 0); - assert_eq!(slot(Tier::new("region/sjc"), Role::Publisher).bytes, 0); + assert_eq!( + slot(Tier::new(crate::Path::from_static("region/sjc")), Role::Publisher).bytes, + 0 + ); let sessions = |tier| { snap.sessions() @@ -2423,7 +2434,10 @@ mod tests { "two default-tier sessions under one label" ); assert_eq!(default_sessions.sessions_ended, 0, "guards still held"); - assert_eq!(sessions(Tier::new("region/sjc")).sessions_started, 1); + assert_eq!( + sessions(Tier::new(crate::Path::from_static("region/sjc"))).sessions_started, + 1 + ); } fn drain(stats: &Registry) -> Report { @@ -2436,8 +2450,10 @@ mod tests { fn report_reuses_capacity() { // A reused report is cleared, not appended to, and keeps its buffers. let stats = test_stats(); - let ctx = stats.tier(Tier::default()).session("root"); - let _scopes: Vec<_> = (0..8).map(|i| ctx.egress(format!("b/{i}").as_str())).collect(); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let _scopes: Vec<_> = (0..8) + .map(|i| ctx.egress(crate::Path::new(format!("b/{i}").as_str()).unwrap())) + .collect(); let mut report = Report::default(); stats.report(&mut report); @@ -2458,9 +2474,9 @@ mod tests { // entry across drains while live, and prunes it on the first drain // after the last guard drops (returning the final values that once). let stats = test_stats(); - let key = PathOwned::from("foo/bar"); - let ctx = stats.tier(Tier::default()).session("root"); - let scope = ctx.egress("foo/bar"); + let key = crate::Path::from_static("foo/bar"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let scope = ctx.egress(crate::Path::from_static("foo/bar")); let sub = scope.subscribe(); scope.meter().bytes(42); @@ -2506,9 +2522,9 @@ mod tests { // the registry indefinitely: announces_started != announces_ended means a // subscription could still begin at any moment. let stats = test_stats(); - let key = PathOwned::from("foo/bar"); - let ctx = stats.tier(Tier::default()).session("root"); - let scope = ctx.egress("foo/bar"); + let key = crate::Path::from_static("foo/bar"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let scope = ctx.egress(crate::Path::from_static("foo/bar")); let guard = scope.announce(); for _ in 0..3 { @@ -2532,7 +2548,7 @@ mod tests { // Once the last session under a label disconnects, the label leaves the // registry on the drain that reports its final gauge. let stats = test_stats(); - let session = stats.tier(Tier::default()).session("acme"); + let session = stats.tier(Tier::default()).session(crate::Path::from_static("acme")); let report = drain(&stats); let row = report @@ -2559,11 +2575,11 @@ mod tests { #[test] fn control_body_totals_follow_retags_and_survive_retirement() { let registry = Registry::new(Default::default()); - let first = Tier::new("internal"); - let second = Tier::new("edge"); - let context = registry.tier(first.clone()).session(""); + let first = Tier::new(crate::Path::from_static("internal")); + let second = Tier::new(crate::Path::from_static("edge")); + let context = registry.tier(first.clone()).session(crate::Path::from_static("")); context.control(Message::Announce, 17, false); - context.retag(second.clone(), ""); + context.retag(second.clone(), crate::Path::from_static("")); context.control(Message::TrackInfo, 23, true); let expected = vec![ (second, Message::TrackInfo, Totals { tx: 0, rx: 23 }), @@ -2580,12 +2596,12 @@ mod tests { fn snapshot_preserves_retired_counters() { let stats = test_stats(); let tier = Tier::default(); - let live = stats.tier(tier.clone()).session("live"); - let scope = live.egress("live/video"); + let live = stats.tier(tier.clone()).session(crate::Path::from_static("live")); + let scope = live.egress(crate::Path::from_static("live/video")); let _live_sub = scope.subscribe(); for _ in 0..2 { - let session = stats.tier(tier.clone()).session("retired"); - let scope = session.egress("retired/video"); + let session = stats.tier(tier.clone()).session(crate::Path::from_static("retired")); + let scope = session.egress(crate::Path::from_static("retired/video")); let sub = scope.subscribe(); scope.meter().bytes(100); drop(sub); @@ -2627,8 +2643,13 @@ mod tests { let worker = std::thread::spawn(move || { for i in 0..256 { let path = format!("root/{i}"); - let session = worker_stats.tier(Tier::default()).session(path.as_str()); - session.ingress(path.as_str()).meter().bytes(1); + let session = worker_stats + .tier(Tier::default()) + .session(crate::Path::new(path.as_str()).unwrap()); + session + .ingress(crate::Path::new(path.as_str()).unwrap()) + .meter() + .bytes(1); drop(session); drain(&worker_stats); } @@ -2673,8 +2694,8 @@ mod tests { // Our own stats broadcasts (and any sibling category under the same // prefix) must not feed back into the registry. let stats = test_stats(); - let ctx = stats.tier(Tier::default()).session("root"); - let scope = ctx.egress(".stats/node/sjc"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let scope = ctx.egress(crate::Path::from_static(".stats/node/sjc")); scope.meter().bytes(100); let _guard = scope.announce(); let _sub = scope.subscribe(); @@ -2687,8 +2708,8 @@ mod tests { // and bumps are dropped. let stats = Registry::default(); assert!(stats.shared.is_none()); - let ctx = stats.tier(Tier::default()).session("root"); - let scope = ctx.egress("demo/bbb"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let scope = ctx.egress(crate::Path::from_static("demo/bbb")); scope.meter().bytes(100); let _guard = scope.announce(); let _sub = scope.subscribe(); @@ -2707,9 +2728,9 @@ mod tests { session_snapshot(&stats, &Tier::default(), label).map(|p| (p.sessions_started, p.sessions_ended)) }; - let a1 = ext.session("acme"); - let a2 = ext.session("acme"); - let b1 = ext.session("globex"); + let a1 = ext.session(crate::Path::from_static("acme")); + let a2 = ext.session(crate::Path::from_static("acme")); + let b1 = ext.session(crate::Path::from_static("globex")); assert_eq!(snap("acme"), Some((2, 0)), "two sessions under one label"); assert_eq!(snap("globex"), Some((1, 0)), "a distinct label is counted separately"); @@ -2784,7 +2805,7 @@ mod tests { session_snapshot(&stats, &Tier::default(), label).map(|p| (p.sessions_started, p.sessions_ended)) }; - let ctx = stats.tier(Tier::default()).session("acme"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("acme")); assert_eq!(snap("acme"), Some((1, 0))); let clone = ctx.clone(); @@ -2799,14 +2820,14 @@ mod tests { #[test] fn retag_moves_presence() { let stats = test_stats(); - let gold = Tier::new("gold"); + let gold = Tier::new(crate::Path::from_static("gold")); let snap = |tier: &Tier| session_snapshot(&stats, tier, "acme").map(|p| (p.sessions_started, p.sessions_ended)); - let ctx = stats.tier(Tier::default()).session("acme"); - ctx.retag(Tier::default(), "acme"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("acme")); + ctx.retag(Tier::default(), crate::Path::from_static("acme")); assert_eq!(snap(&Tier::default()), Some((1, 0)), "the same tier is a no-op"); - ctx.retag(gold.clone(), "acme"); + ctx.retag(gold.clone(), crate::Path::from_static("acme")); assert_eq!(snap(&Tier::default()), Some((1, 1))); assert_eq!(snap(&gold), Some((1, 0))); @@ -2817,9 +2838,9 @@ mod tests { #[test] fn retag_moves_subsequent_traffic() { let stats = test_stats(); - let gold = Tier::new("gold"); - let ctx = stats.tier(Tier::default()).session("acme"); - let scope = ctx.egress("demo/bbb"); + let gold = Tier::new(crate::Path::from_static("gold")); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("acme")); + let scope = ctx.egress(crate::Path::from_static("demo/bbb")); let clone = scope.clone(); let before = scope.meter(); @@ -2827,7 +2848,7 @@ mod tests { let announce = scope.announce(); before.bytes(10); - ctx.retag(gold.clone(), "acme"); + ctx.retag(gold.clone(), crate::Path::from_static("acme")); // A meter handed out earlier keeps its tier; everything after moves. before.bytes(1); scope.meter().bytes(5); @@ -2864,11 +2885,19 @@ mod tests { fn retag_moves_presence_between_labels_on_one_tier() { let stats = test_stats(); let tier = Tier::default(); - let session = stats.tier(tier.clone()).session("project/root"); - session.egress("project/live").meter().bytes(5); - - session.retag(tier.clone(), "project/key/123"); - session.egress("project/live").meter().bytes(7); + let session = stats + .tier(tier.clone()) + .session(crate::Path::from_static("project/root")); + session + .egress(crate::Path::from_static("project/live")) + .meter() + .bytes(5); + + session.retag(tier.clone(), crate::Path::from_static("project/key/123")); + session + .egress(crate::Path::from_static("project/live")) + .meter() + .bytes(7); let root = session_snapshot(&stats, &tier, "project/root").unwrap(); let key = session_snapshot(&stats, &tier, "project/key/123").unwrap(); @@ -2880,12 +2909,12 @@ mod tests { fn retag_back_counts_bytes_once() { let stats = test_stats(); let tier = Tier::default(); - let session = stats.tier(tier.clone()).session("a"); - let meter = session.ingress("a/live").meter(); + let session = stats.tier(tier.clone()).session(crate::Path::from_static("a")); + let meter = session.ingress(crate::Path::from_static("a/live")).meter(); meter.bytes(1); - session.retag(tier.clone(), "b"); + session.retag(tier.clone(), crate::Path::from_static("b")); meter.bytes(2); - session.retag(tier.clone(), "a"); + session.retag(tier.clone(), crate::Path::from_static("a")); meter.bytes(4); let received = |label| session_snapshot(&stats, &tier, label).unwrap().received_bytes; @@ -2896,8 +2925,8 @@ mod tests { fn session_bytes_outlive_the_session() { let stats = test_stats(); let tier = Tier::default(); - let session = stats.tier(tier.clone()).session("acme"); - let meter = session.egress("acme/live").meter(); + let session = stats.tier(tier.clone()).session(crate::Path::from_static("acme")); + let meter = session.egress(crate::Path::from_static("acme/live")).meter(); meter.bytes(3); drop(session); @@ -2921,8 +2950,8 @@ mod tests { fn excluded_paths_skip_session_bytes() { let stats = test_stats(); let tier = Tier::default(); - let session = stats.tier(tier.clone()).session("acme"); - session.egress(".stats/node").meter().bytes(9); + let session = stats.tier(tier.clone()).session(crate::Path::from_static("acme")); + session.egress(crate::Path::from_static(".stats/node")).meter().bytes(9); assert_eq!(session_snapshot(&stats, &tier, "acme").unwrap().sent_bytes, 0); } @@ -2930,14 +2959,14 @@ mod tests { fn meter_bumps_the_right_side() { // A payload meter records on its own side only. let stats = test_stats(); - let ctx = stats.tier(Tier::default()).session("root"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); - let egress = ctx.egress("demo/bbb").meter(); + let egress = ctx.egress(crate::Path::from_static("demo/bbb")).meter(); egress.group(); egress.frames(3); egress.bytes(100); - let ingress = ctx.ingress("demo/bbb").meter(); + let ingress = ctx.ingress(crate::Path::from_static("demo/bbb")).meter(); ingress.group(); ingress.frames(1); ingress.bytes(7); @@ -2954,10 +2983,10 @@ mod tests { // An egress subscription bumps `subscriptions_started` and, being the context's first // for the broadcast, `broadcasts_started`. Dropping closes both. let stats = test_stats(); - let ctx = stats.tier(Tier::default()).session("root"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot(); - let scope = ctx.egress("demo/bbb"); + let scope = ctx.egress(crate::Path::from_static("demo/bbb")); let s1 = scope.subscribe(); let s2 = scope.subscribe(); let r = raw(); @@ -2979,9 +3008,17 @@ mod tests { let stats = test_stats(); let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot(); - let v1 = stats.tier(Tier::default()).session("a").egress("demo/bbb").subscribe(); + let v1 = stats + .tier(Tier::default()) + .session(crate::Path::from_static("a")) + .egress(crate::Path::from_static("demo/bbb")) + .subscribe(); assert_eq!(raw().broadcasts_started, 1); - let v2 = stats.tier(Tier::default()).session("b").egress("demo/bbb").subscribe(); + let v2 = stats + .tier(Tier::default()) + .session(crate::Path::from_static("b")) + .egress(crate::Path::from_static("demo/bbb")) + .subscribe(); assert_eq!(raw().broadcasts_started, 2, "two distinct contexts => two viewers"); drop(v1); @@ -2995,8 +3032,8 @@ mod tests { // An ingress (producer-lifetime) subscription bumps subscriptions but never // the viewer refcount, which is egress-only. let stats = test_stats(); - let ctx = stats.tier(Tier::default()).session("root"); - let guard = ctx.ingress("demo/bbb").subscribe(); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let guard = ctx.ingress(crate::Path::from_static("demo/bbb")).subscribe(); let sub = tier_counters(&stats, "demo/bbb", &Tier::default()) .subscriber .snapshot(); @@ -3015,17 +3052,22 @@ mod tests { #[test] fn request_admission_survives_clones_retiering_and_retirement_without_presence() { let stats = test_stats(); - let request = stats.tier(Tier::new("old")).request("root"); + let request = stats + .tier(Tier::new(crate::Path::from_static("old"))) + .request(crate::Path::from_static("root")); let clone = request.clone(); - request.egress("root/video").meter().bytes(7); - request.retag(Tier::new("new"), "root"); - clone.egress("root/video").meter().bytes(11); + request.egress(crate::Path::from_static("root/video")).meter().bytes(7); + request.retag( + Tier::new(crate::Path::from_static("new")), + crate::Path::from_static("root"), + ); + clone.egress(crate::Path::from_static("root/video")).meter().bytes(11); drop(request); drop(clone); let mut report = Report::default(); stats.report(&mut report); for (label, requests, bytes) in [("old", 1, 7), ("new", 0, 11)] { - let tier = Tier::new(label); + let tier = Tier::new(crate::Path::new(label).unwrap()); let presence = report.sessions.iter().find(|entry| entry.tier == tier).unwrap(); assert_eq!(presence.presence.requests, requests); assert_eq!(presence.presence.active(), 0); @@ -3075,8 +3117,8 @@ mod tests { fn fetch_counts_separately_from_subscriptions() { // A fetch bumps `fetches`, not `subscriptions_started` or the viewer refcount. let stats = test_stats(); - let ctx = stats.tier(Tier::default()).session("root"); - let scope = ctx.egress("demo/bbb"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); + let scope = ctx.egress(crate::Path::from_static("demo/bbb")); scope.fetch(); scope.fetch(); let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot(); @@ -3090,10 +3132,10 @@ mod tests { // The announce guard bumps `announces_started` + the path length on open, and // `announces_ended` + the path length again on drop. let stats = test_stats(); - let ctx = stats.tier(Tier::default()).session("root"); + let ctx = stats.tier(Tier::default()).session(crate::Path::from_static("root")); let path_len = "demo/bbb".len() as u64; - let guard = ctx.egress("demo/bbb").announce(); + let guard = ctx.egress(crate::Path::from_static("demo/bbb")).announce(); let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot(); assert_eq!(r.announces_started, 1); assert_eq!(r.announces_ended, 0); @@ -3113,7 +3155,7 @@ mod tests { fn disabled_context_is_noop() { // A default (disabled) context resolves empty scopes: every bump is dropped. let ctx = Session::default(); - let scope = ctx.egress("demo/bbb"); + let scope = ctx.egress(crate::Path::from_static("demo/bbb")); scope.meter().bytes(100); let _guard = scope.announce(); let _sub = scope.subscribe(); diff --git a/rs/moq-net/src/test_interop.rs b/rs/moq-net/src/test_interop.rs index 2c1a45ff166..900aa595d1b 100644 --- a/rs/moq-net/src/test_interop.rs +++ b/rs/moq-net/src/test_interop.rs @@ -57,7 +57,7 @@ fn ietf_range_updates_interop() { w.varint(ietf::Subscribe::ID).unwrap(); ietf::Subscribe { request_id: RequestId(0), - track_namespace: crate::Path::new("room"), + track_namespace: crate::Path::from_static("room"), track_name: "video".into(), subscriber_priority: 127, group_order: ietf::GroupOrder::Ascending, @@ -569,7 +569,7 @@ fn lite_tracks_interop() { let version = lite::Version::Lite07; let request = lite::Tracks { - broadcast: crate::Path::new("room"), + broadcast: crate::Path::from_static("room"), epoch: None, } .encode_bytes(version) @@ -650,7 +650,7 @@ fn ietf_track_status_interop() { w.varint(::ID).unwrap(); ietf::TrackStatus { request_id: ietf::RequestId(id), - track_namespace: crate::Path::new(namespace), + track_namespace: crate::Path::new(namespace).unwrap(), track_name: "video".into(), properties_wanted: wanted, epoch: None, diff --git a/rs/moq-net/src/transport.rs b/rs/moq-net/src/transport.rs index 8d99f53a497..85a029dd9fb 100644 --- a/rs/moq-net/src/transport.rs +++ b/rs/moq-net/src/transport.rs @@ -175,7 +175,7 @@ impl Metrics for Untracked {} pub const MIN_RECEIVE_WINDOW: u64 = { // A type and a size prefix, each a varint of at most 8 bytes. const PREFIXES: u64 = 16; - let message = as crate::lite::Message>::MAX_SIZE as u64 + PREFIXES; + let message = ::MAX_SIZE as u64 + PREFIXES; let segment = crate::frame::SEGMENT as u64; if message > segment { message } else { segment } }; diff --git a/rs/moq-net/tests/integration/compress.rs b/rs/moq-net/tests/integration/compress.rs index 22bddb5f8c8..3ddde6d5f9e 100644 --- a/rs/moq-net/tests/integration/compress.rs +++ b/rs/moq-net/tests/integration/compress.rs @@ -296,7 +296,9 @@ async fn datagrams_meter_the_bytes_on_the_wire() { let mut options = MockConnectOptions::new("moq-lite-07-wip".parse::().unwrap()); options.client_publish = Some(publisher.consume()); options.server_subscribe = Some(relay.clone()); - options.server_stats = ingress.tier(Default::default()).session("ingress"); + options.server_stats = ingress + .tier(Default::default()) + .session(moq_net::Path::from_static("ingress")); let upstream = connect_mock(options).await; let origin = produce_origin(3); @@ -304,7 +306,9 @@ async fn datagrams_meter_the_bytes_on_the_wire() { let mut options = MockConnectOptions::new(version.parse::().unwrap()); options.server_publish = Some(relay.consume()); options.client_subscribe = Some(origin.clone()); - options.server_stats = egress.tier(Default::default()).session("egress"); + options.server_stats = egress + .tier(Default::default()) + .session(moq_net::Path::from_static("egress")); let downstream = connect_mock(options).await; let consumer = origin.consume(); diff --git a/rs/moq-net/tests/integration/control_stats.rs b/rs/moq-net/tests/integration/control_stats.rs index eac3fdc4ebc..01239c176ec 100644 --- a/rs/moq-net/tests/integration/control_stats.rs +++ b/rs/moq-net/tests/integration/control_stats.rs @@ -31,9 +31,9 @@ async fn control_classes_cross_lite_and_ietf_sessions_without_counting_payload() let publisher = origin(); let subscriber = origin(); let registry = Registry::new(Default::default()); - let tier = Tier::new("internal"); + let tier = Tier::new(moq_net::Path::from_static("internal")); let mut options = MockConnectOptions::new(version.parse().unwrap()); - options.server_stats = registry.tier(tier.clone()).session(""); + options.server_stats = registry.tier(tier.clone()).session(moq_net::Path::from_static("")); options.client_publish = Some(publisher.consume()); options.server_subscribe = Some(subscriber.clone()); let _pair = connect_mock(options).await; @@ -79,13 +79,13 @@ async fn setup_counts_under_the_admitted_stats() { for version in ["moq-lite-07-wip", "moq-transport-14", "moq-transport-22"] { let version: moq_net::Version = version.parse().unwrap(); let registry = Registry::new(Default::default()); - let tier = Tier::new("internal"); + let tier = Tier::new(moq_net::Path::from_static("internal")); let mut transport = moq_net::transport::memory::Config::default(); transport.protocol = Some(version.alpn().to_string()); let (client_transport, server_transport) = create_mock_session_pair(transport); let client = moq_net::Client::new().with_versions(version.into()); let server = moq_net::Server::new().with_versions(version.into()); - let stats = registry.tier(tier.clone()).session(""); + let stats = registry.tier(tier.clone()).session(moq_net::Path::from_static("")); let client = async { let (session, driver) = client.connect(now(), client_transport).await.unwrap(); spawn(driver); diff --git a/rs/moq-net/tests/integration/dial_split_horizon.rs b/rs/moq-net/tests/integration/dial_split_horizon.rs index ca4df3e39e7..5dc11bff6e3 100644 --- a/rs/moq-net/tests/integration/dial_split_horizon.rs +++ b/rs/moq-net/tests/integration/dial_split_horizon.rs @@ -43,7 +43,9 @@ struct Link { async fn dial(dialer: &origin::Producer, edge: &origin::Producer) -> Link { let version = version(); let registry = stats::Registry::new(stats::Config::new()); - let session_stats = registry.tier(stats::Tier::default()).session(""); + let session_stats = registry + .tier(stats::Tier::default()) + .session(moq_net::Path::from_static("")); let mut mock = moq_net::transport::memory::Config::default(); mock.protocol = Some(version.alpn().to_string()); let (client_transport, server_transport) = create_mock_session_pair(mock); diff --git a/rs/moq-net/tests/integration/idle_session.rs b/rs/moq-net/tests/integration/idle_session.rs index 593ee2ead4f..8429bd0340c 100644 --- a/rs/moq-net/tests/integration/idle_session.rs +++ b/rs/moq-net/tests/integration/idle_session.rs @@ -179,7 +179,9 @@ async fn wire_collection_never_arms_the_idle_sampler() { let registry = moq_net::stats::Registry::new(Default::default()); let polls = Arc::new(AtomicUsize::new(0)); let mut options = MockConnectOptions::new(version.parse().unwrap()); - options.server_stats = registry.tier(Default::default()).session("room"); + options.server_stats = registry + .tier(Default::default()) + .session(moq_net::Path::from_static("room")); options.server_polls = Some(polls.clone()); let pair = connect_mock(options).await; moq_clock::sleep(Duration::from_secs(1)).await; diff --git a/rs/moq-net/tests/integration/session_close.rs b/rs/moq-net/tests/integration/session_close.rs index bbafd3dcf81..421cc5416c2 100644 --- a/rs/moq-net/tests/integration/session_close.rs +++ b/rs/moq-net/tests/integration/session_close.rs @@ -433,7 +433,9 @@ async fn an_unknown_lite_range_abort_is_unspecified_on_every_wire() { async fn ended(version: &str, close: impl FnOnce(&MockPair)) -> (Error, moq_net::stats::Failures) { let registry = moq_net::stats::Registry::new(Default::default()); let mut options = MockConnectOptions::new(version.parse().unwrap()); - options.server_stats = registry.tier(Default::default()).session("room"); + options.server_stats = registry + .tier(Default::default()) + .session(moq_net::Path::from_static("room")); let pair = connect_mock(options).await; close(&pair); let err = moq_clock::timeout(TIMEOUT, pair.server.closed()) diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index c244598804a..d25e58b965d 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -5,6 +5,10 @@ Rolling a cluster across a release also follows the [upgrade guide](https://moq. ## Unreleased +### `moq_net::Path` has no lifetime + +`auth` token fields that were `PathOwned` are `moq_net::Path`. `auth::Token::new` and `auth::Lease::new` return `Result`, refusing a grant whose root, mount, tier, or label is longer than `moq_net::Path::MAX_BYTES`. See moq-net's migration for the rest. + ### Tiers are metric-safe names (#6778) `--cluster-tier` and the auth server's grant and refusal `tier` must be `/`-joined segments of ASCII letters, digits, `.`, `_`, and `-`. diff --git a/rs/moq-relay/src/auth.rs b/rs/moq-relay/src/auth.rs index ae91683ac1a..5490982a2bf 100644 --- a/rs/moq-relay/src/auth.rs +++ b/rs/moq-relay/src/auth.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use axum::http; use moq_auth::{Bytes, Grant, Request, lease}; use moq_auth::{Pattern, Patterns}; -use moq_net::{Path, PathOwned, stats::Tier}; +use moq_net::{Path, stats::Tier}; use serde::{Deserialize, Serialize}; use serde_with::{OneOrMany, serde_as}; use tokio::sync::{mpsc, oneshot}; @@ -427,10 +427,10 @@ pub struct Token { /// The path the session dialed, which a grant without `root` is relative to. pub(crate) path: String, /// The root the session is scoped to: the grant's `root` alias, else the dialed path. - pub root: PathOwned, + pub root: Path, /// Subtrees read from elsewhere on the origin: each path, relative to `root`, /// resolves at the absolute path it maps to. - pub mounts: Vec<(PathOwned, PathOwned)>, + pub mounts: Vec<(Path, Path)>, /// Patterns the session may subscribe to, relative to `root`. pub subscribe: moq_auth::Patterns, /// Patterns the session may publish, relative to `root`. @@ -438,7 +438,7 @@ pub struct Token { /// The tier this session's stats record under. pub tier: Tier, /// The path this session's stats record under, empty when the grant names none. - pub label: PathOwned, + pub label: Path, /// Whether the session is a cluster peer, so its routes entered elsewhere. pub peer: bool, /// Whether the peer is upstream, so it is never offered another upstream's routes. @@ -451,28 +451,33 @@ impl Token { /// The token keeps the patterns the grant yields: the origin scopes by any /// pattern union, so `alice`, `*/chat`, and `alice/**` each mean exactly what /// they say. - pub fn new(path: &str, grant: &Grant) -> Self { + /// + /// Fails if the dialed path, or a root, mount, tier, or label the grant names, is + /// longer than [`Path::MAX_BYTES`]. + pub fn new(path: &str, grant: &Grant) -> Result { + let too_long = |err: moq_net::path::TooLong| Error::Forbidden(err.to_string()); let root = grant.root.as_deref().unwrap_or(path); - Self { + Ok(Self { path: path.to_string(), - root: Path::new(root).to_owned(), + root: Path::new(root).map_err(too_long)?, mounts: grant .mounts .iter() - .map(|(at, target)| (Path::new(at).to_owned(), Path::new(target).to_owned())) - .collect(), + .map(|(at, target)| Ok((Path::new(at)?, Path::new(target)?))) + .collect::>() + .map_err(too_long)?, subscribe: grant.subscribe.clone(), publish: grant.publish.clone(), - tier: crate::configured_tier(grant.tier.clone()), - label: Path::new(grant.label.as_deref().unwrap_or_default()).to_owned(), + tier: crate::configured_tier(grant.tier.clone()).map_err(too_long)?, + label: Path::new(grant.label.as_deref().unwrap_or_default()).map_err(too_long)?, peer: grant.peer, upstream: grant.upstream, - } + }) } /// Rebuild the token from a re-checked grant, relative to the same dialed path, /// so a grant that drops its `root` alias resolves back to what was dialed. - pub(crate) fn recheck(&self, grant: &Grant) -> Self { + pub(crate) fn recheck(&self, grant: &Grant) -> Result { Self::new(&self.path, grant) } @@ -517,23 +522,25 @@ struct Source { impl Lease { /// Hold `consumer` for a session that dialed `path`, reducing its grant to /// what the origin scopes by. - pub fn new(path: &str, consumer: lease::Consumer) -> Self { + /// + /// Fails if the grant's paths are too long; see [`Token::new`]. + pub fn new(path: &str, consumer: lease::Consumer) -> Result { let grant = consumer.grant(); Self::granted(path, &grant, consumer) } /// Hold `consumer` with its token built from `grant`, a snapshot of it. A later /// update still reaches [`Self::ended`] as a change, which re-checks it. - fn granted(path: &str, grant: &Grant, consumer: lease::Consumer) -> Self { - Self { + fn granted(path: &str, grant: &Grant, consumer: lease::Consumer) -> Result { + Ok(Self { refresh: None, source: None, - token: Token::new(path, grant), + token: Token::new(path, grant)?, expires: grant.deadline(), consumer, session: None, stats: Default::default(), - } + }) } /// Take AUTH decisions before the protocol driver chooses its default acceptor. @@ -615,7 +622,10 @@ impl Lease { if let Err(reason) = still_valid(&grant) { return reason; } - let fresh = self.token.recheck(&grant); + let fresh = match self.token.recheck(&grant) { + Ok(fresh) => fresh, + Err(err) => return err.to_string().into(), + }; if fresh.root != self.token.root { return lease::Reason::Narrowed; } @@ -887,7 +897,7 @@ impl Auth { // Build the token from the grant the admission checked: re-reading it could // pick up an update that skipped the check. let (consumer, grant) = self.decide(request.clone()).await?; - let mut lease = Lease::granted(&request.path, &grant, consumer); + let mut lease = Lease::granted(&request.path, &grant, consumer)?; lease.source = Some(Source { auth: self.clone(), request, @@ -916,7 +926,7 @@ impl Auth { /// Admit a session on a grant decided locally, bypassing the server: the LAN /// mesh credential, which the relay minted for itself. pub(crate) fn admit_fixed(&self, path: &str, grant: Grant) -> Lease { - Lease::new(path, lease::Consumer::fixed(grant)) + Lease::new(path, lease::Consumer::fixed(grant)).expect("the relay's own grant fits") } } @@ -1115,7 +1125,7 @@ mod tests { async fn an_invalid_label_at_revalidation_ends_the_lease() { let grant = Grant::new(moq_net::Pattern::all(), moq_net::Pattern::all()); let (producer, consumer) = lease::Producer::new(grant.clone()); - let mut lease = Lease::new("/room", consumer); + let mut lease = Lease::new("/room", consumer).unwrap(); let mut wildcard = grant; wildcard.label = Some("project/*".into()); producer.update(wildcard); @@ -1233,7 +1243,7 @@ mod tests { ("/rooms", "rooms", &[], &["chat"]), ] { let lease = auth.admit(auth.request(moq_auth::Transport::Quic, path)).await.unwrap(); - assert_eq!(lease.token().root, Path::new(root).to_owned(), "{path}"); + assert_eq!(lease.token().root, Path::new(root).unwrap(), "{path}"); assert_eq!(lease.token().publish, patterns(publish), "{path}"); assert_eq!(lease.token().subscribe, patterns(subscribe), "{path}"); } @@ -1293,7 +1303,7 @@ mod tests { }); for request in [anonymous, certificate] { let lease = auth.admit(request).await.unwrap(); - assert_eq!(lease.token().root, Path::new("anon/room").to_owned()); + assert_eq!(lease.token().root, Path::from_static("anon/room")); assert_eq!(lease.token().subscribe, patterns(&["**"])); assert_eq!(lease.token().tier, Tier::default()); } @@ -1358,7 +1368,7 @@ mod tests { }; let admit = async { let lease = auth.admit(request()).await.expect("granted"); - assert_eq!(lease.token().root, Path::new("anon/room").to_owned()); + assert_eq!(lease.token().root, Path::from_static("anon/room")); assert!(matches!(auth.admit(request()).await, Err(Error::Refused(_)))); assert!(matches!(auth.admit(request()).await, Err(Error::Refused(_)))); assert!(matches!(auth.admit(request()).await, Err(Error::Unavailable(_)))); @@ -1399,7 +1409,7 @@ mod tests { let start = moq_clock::now(); let mut grant = Grant::new(patterns(&["**"]), patterns(&["**"])); grant.expires = Some(moq_clock::wall() + Duration::from_secs(10)); - let mut lease = Lease::new("/room", lease::Consumer::fixed(grant)); + let mut lease = Lease::new("/room", lease::Consumer::fixed(grant)).unwrap(); for _ in 0..9 { assert!(moq_clock::timeout(Duration::from_secs(1), lease.ended()).await.is_err()); } @@ -1420,7 +1430,7 @@ mod tests { assert!(matches!(grant.validate(), Err(moq_auth::Error::GrantExpired))); let start = moq_clock::now(); - let mut lease = Lease::new("/room", lease::Consumer::fixed(grant)); + let mut lease = Lease::new("/room", lease::Consumer::fixed(grant)).unwrap(); assert_eq!(lease.ended().await, lease::Reason::Expired); assert_eq!(moq_clock::now(), start, "no grace after expiry"); } @@ -1431,15 +1441,15 @@ mod tests { grant.root = Some("pid/room".into()); grant.tier = Some("gold".into()); grant.label = Some("project/key/123".into()); - let token = Token::new("/vanity/room", &grant); - assert_eq!(token.root, Path::new("pid/room").to_owned()); + let token = Token::new("/vanity/room", &grant).unwrap(); + assert_eq!(token.root, Path::from_static("pid/room")); assert_eq!(token.publish, patterns(&["alice/**"])); assert_eq!(token.subscribe, patterns(&["**"])); - assert_eq!(token.tier, Tier::new("gold")); - assert_eq!(token.label, Path::new("project/key/123").to_owned()); + assert_eq!(token.tier, Tier::new(moq_net::Path::from_static("gold"))); + assert_eq!(token.label, Path::from_static("project/key/123")); let patterns = patterns(&["*/chat", "alice", ""]); - let token = Token::new("/", &Grant::new(patterns.clone(), Patterns::new())); + let token = Token::new("/", &Grant::new(patterns.clone(), Patterns::new())).unwrap(); assert_eq!(token.publish, patterns); assert!( token.label.is_empty(), @@ -1449,15 +1459,15 @@ mod tests { #[test] fn a_narrower_recheck_is_not_covered() { - let wide = Token::new("/room", &Grant::new(patterns(&["**"]), patterns(&["**"]))); - let narrow = Token::new("/room", &Grant::new(patterns(&["alice/**"]), patterns(&["**"]))); - let moved = Token::new("/other", &Grant::new(patterns(&["**"]), patterns(&["**"]))); + let wide = Token::new("/room", &Grant::new(patterns(&["**"]), patterns(&["**"]))).unwrap(); + let narrow = Token::new("/room", &Grant::new(patterns(&["alice/**"]), patterns(&["**"]))).unwrap(); + let moved = Token::new("/other", &Grant::new(patterns(&["**"]), patterns(&["**"]))).unwrap(); let mut mounted = Grant::new(patterns(&["**"]), patterns(&["**"])); mounted.mounts.insert(".svc".into(), ".svc/room".into()); - let mounted = Token::new("/room", &mounted); + let mounted = Token::new("/room", &mounted).unwrap(); assert_eq!( mounted.mounts, - [(Path::new(".svc").to_owned(), Path::new(".svc/room").to_owned())] + [(Path::from_static(".svc"), Path::from_static(".svc/room"))] ); assert!(wide.covered_by(&wide)); assert!(narrow.covered_by(&wide)); @@ -1474,7 +1484,7 @@ mod tests { async fn a_recheck_that_flips_the_link_closes() { let grant = Grant::new(patterns(&["**"]), patterns(&["**"])); let (producer, consumer) = lease::Producer::new(grant.clone()); - let mut lease = Lease::new("/", consumer); + let mut lease = Lease::new("/", consumer).unwrap(); assert!(!lease.token().peer); let mut peer = grant; @@ -1485,7 +1495,7 @@ mod tests { // Routes already offered to (or withheld from) the session were filtered // by whether it is upstream, so flipping that closes it too. let (producer, consumer) = lease::Producer::new(peer.clone()); - let mut lease = Lease::new("/", consumer); + let mut lease = Lease::new("/", consumer).unwrap(); let mut upstream = peer; upstream.upstream = true; producer.update(upstream); @@ -1497,13 +1507,16 @@ mod tests { let everything = || Grant::new(patterns(&["**"]), patterns(&["**"])); let mut aliased = everything(); aliased.root = Some("pid/room".into()); - let token = Token::new("/vanity/room", &aliased); - assert_eq!(token.root, Path::new("pid/room").to_owned()); + let token = Token::new("/vanity/room", &aliased).unwrap(); + assert_eq!(token.root, Path::from_static("pid/room")); // The same alias still names the same root. - assert_eq!(token.recheck(&aliased).root, token.root); + assert_eq!(token.recheck(&aliased).unwrap().root, token.root); // A grant without the alias is relative to what was dialed, not to the old root. - assert_eq!(token.recheck(&everything()).root, Path::new("vanity/room").to_owned()); + assert_eq!( + token.recheck(&everything()).unwrap().root, + Path::from_static("vanity/room") + ); } } diff --git a/rs/moq-relay/src/auth/admission_tests.rs b/rs/moq-relay/src/auth/admission_tests.rs index 1bec4316f4a..67f2fcafc33 100644 --- a/rs/moq-relay/src/auth/admission_tests.rs +++ b/rs/moq-relay/src/auth/admission_tests.rs @@ -111,7 +111,7 @@ async fn a_moved_gateway_tier_retags_its_stats() { .snapshot() .sessions() .into_iter() - .find(|(t, _)| *t == stats::Tier::new(tier)) + .find(|(t, _)| *t == stats::Tier::new(moq_net::Path::from_static(tier))) .map_or(0, |(_, presence)| presence.active()) }; diff --git a/rs/moq-relay/src/auth/refresh.rs b/rs/moq-relay/src/auth/refresh.rs index 4c1b1b39439..be3ae4845b6 100644 --- a/rs/moq-relay/src/auth/refresh.rs +++ b/rs/moq-relay/src/auth/refresh.rs @@ -130,7 +130,9 @@ impl Refresh { } fn valid(&self, grant: &Grant) -> bool { - let fresh = self.admitted.recheck(grant); + let Ok(fresh) = self.admitted.recheck(grant) else { + return false; + }; super::still_valid(grant).is_ok() && fresh.root == self.admitted.root && fresh.mounts == self.admitted.mounts @@ -371,7 +373,7 @@ impl Refresh { } } Event::Changed(id, Change::Grant(grant)) if self.valid(&grant) => { - let fresh = self.admitted.recheck(&grant); + let fresh = self.admitted.recheck(&grant).expect("checked by valid"); let credential = self.credentials.get_mut(&id).expect("observed credential"); credential.expires = grant.deadline(); if credential.initial { @@ -624,8 +626,12 @@ mod tests { let initial = grant("a/**", 1); let (producer, consumer) = lease::Producer::new(initial.clone()); let registry = moq_net::stats::Registry::new(Default::default()); - let stats = registry.tier(moq_net::stats::Tier::default()).session("room"); - let lease = super::super::Lease::new("/room", consumer).with_stats(stats.clone()); + let stats = registry + .tier(moq_net::stats::Tier::default()) + .session(moq_net::Path::from_static("room")); + let lease = super::super::Lease::new("/room", consumer) + .unwrap() + .with_stats(stats.clone()); let (client_transport, server) = transports().await; let now = moq_net::time::Instant::now(); let origin = moq_tokio::origin::spawn(); @@ -736,8 +742,10 @@ mod tests { ] { let (producer, consumer) = lease::Producer::new(grant("a/**", 3000)); let registry = moq_net::stats::Registry::new(Default::default()); - let stats = registry.tier(moq_net::stats::Tier::default()).session("room"); - let lease = super::super::Lease::new("/room", consumer).with_stats(stats); + let stats = registry + .tier(moq_net::stats::Tier::default()) + .session(moq_net::Path::from_static("room")); + let lease = super::super::Lease::new("/room", consumer).unwrap().with_stats(stats); let (client, supervising) = sessions(lease).await; producer.revoke(reason.clone()); supervising.await.unwrap(); @@ -947,7 +955,9 @@ mod tests { admissions.next().await.unwrap().grant(consumer); }); let registry = moq_net::stats::Registry::new(Default::default()); - let stats = registry.tier(moq_net::stats::Tier::default()).session("room"); + let stats = registry + .tier(moq_net::stats::Tier::default()) + .session(moq_net::Path::from_static("room")); let (client, session, mut lease) = connect(lease.unwrap().with_stats(stats)).await; let auth = client.auth(); let (token, ()) = tokio::join!( diff --git a/rs/moq-relay/src/cluster.rs b/rs/moq-relay/src/cluster.rs index e941f83bb1b..638dc2edcad 100644 --- a/rs/moq-relay/src/cluster.rs +++ b/rs/moq-relay/src/cluster.rs @@ -11,7 +11,7 @@ use std::{ use anyhow::Context; use futures::FutureExt as _; use moq_net::origin; -use moq_net::{Hop, stats::Tier}; +use moq_net::{Hop, Path, stats::Tier}; use reqwest_middleware::ClientWithMiddleware; use tokio::task::AbortHandle; use tracing::Instrument as _; @@ -1234,6 +1234,7 @@ impl Cluster { if let Some(tier) = &config.tier { moq_auth::tier::validate(tier).context("invalid --cluster-tier")?; } + crate::configured_tier(config.tier.clone()).context("invalid --cluster-tier")?; peer.quic.validate()?; let critical = crate::critical::Critical::default(); let mut origin_config = origin::Config::new(id); @@ -1438,7 +1439,7 @@ impl Cluster { /// Billing tier cluster-peer traffic records under (`--cluster-tier`). /// An absent or empty label selects the default unprefixed tier. fn cluster_tier(&self) -> Tier { - crate::configured_tier(self.config.tier.clone()) + crate::configured_tier(self.config.tier.clone()).expect("validated by Cluster::new") } /// Returns an [`origin::Producer`] scoped to this session's subscribe permissions. @@ -1468,7 +1469,8 @@ impl Cluster { (false, false) => self.origin.clone(), }; for (at, target) in &token.mounts { - origin = match origin.mount(token.root.join(at), target) { + let mounted = token.root.join(at).map_err(moq_net::Error::from); + origin = match mounted.and_then(|at| origin.mount(at, target)) { Ok(origin) => origin, Err(err) => { tracing::warn!(root = %token.root, %at, %target, %err, "grant mount refused"); @@ -2033,7 +2035,7 @@ impl Cluster { }; // The link is one presence across reconnects: each session it dials records how it // ended, and the last one to end decides whether the link failed. - let stats = self.stats.tier(self.cluster_tier()).session(""); + let stats = self.stats.tier(self.cluster_tier()).session(Path::empty()); let mut client = client .with_publisher(origin.consume().with_hidden(true)) .with_subscriber(origin) @@ -2338,7 +2340,7 @@ mod tests { grant.upstream = upstream; let (_producer, consumer) = moq_auth::lease::Producer::new(grant); // A cluster-looking path alone never selects the peer policy. - let lease = auth::Lease::new("/.cluster/untrusted", consumer); + let lease = auth::Lease::new("/.cluster/untrusted", consumer).unwrap(); let limits = cluster.limits(&lease); assert_eq!((limits.announces, limits.requests), expected); } @@ -2362,7 +2364,7 @@ mod tests { for session in [false, true] { let mut grant = moq_auth::Grant::new(moq_auth::Pattern::all(), moq_auth::Pattern::all()); grant.peer = peer; - let lease = auth::Lease::new("/", moq_auth::lease::Consumer::fixed(grant)); + let lease = auth::Lease::new("/", moq_auth::lease::Consumer::fixed(grant)).unwrap(); let admitted = match session { true => cluster.scope_session(lease, &request), false => cluster.scope(lease, &request), @@ -2415,7 +2417,7 @@ mod tests { let everything = || moq_net::Patterns::from(moq_net::Pattern::all()); let mut grant = moq_auth::Grant::new(everything(), everything()); grant.mounts.insert(".svc".into(), ".svc/pid".into()); - let token = auth::Token::new("/pid", &grant); + let token = auth::Token::new("/pid", &grant).unwrap(); let _worker = cluster .origin @@ -2434,7 +2436,7 @@ mod tests { // A mount the origin refuses admits nothing rather than half a grant. grant.mounts.insert(".svc/x".into(), ".other".into()); - let token = auth::Token::new("/pid", &grant); + let token = auth::Token::new("/pid", &grant).unwrap(); assert!(cluster.subscriber(&token).is_none()); assert!(cluster.publisher(&token).is_none()); } @@ -2452,7 +2454,7 @@ mod tests { for (at, target) in mounts { grant.mounts.insert((*at).into(), (*target).into()); } - let token = auth::Token::new("/pid", &grant); + let token = auth::Token::new("/pid", &grant).unwrap(); assert!(cluster.subscriber(&token).is_none(), "{mounts:?}"); assert!(cluster.publisher(&token).is_none(), "{mounts:?}"); } @@ -2481,7 +2483,7 @@ mod tests { let stats = config.build(cluster.origin.clone()).expect("stats"); let cluster = cluster.with_stats(stats); - let path = moq_net::Path::new(".stats").join("node").join("test"); + let path = moq_net::Path::from_static(".stats/node/test"); let consumer = cluster.origin.consume(); let route = tokio::time::timeout(std::time::Duration::from_secs(5), consumer.routed(&path)) .await @@ -2557,7 +2559,10 @@ mod tests { ..Default::default() }) .expect("cluster"); - assert_eq!(cluster.cluster_tier(), Tier::new("region/sjc")); + assert_eq!( + cluster.cluster_tier(), + Tier::new(moq_net::Path::from_static("region/sjc")) + ); let quoted = new_cluster(Config { tier: Some("region\"".to_string()), @@ -3090,7 +3095,7 @@ mod tests { .expect("broadcast resolves") .expect("broadcast present"); - let stats_path = moq_net::Path::new(".stats").join("node").join("test"); + let stats_path = moq_net::Path::from_static(".stats/node/test"); tokio::time::timeout(Duration::from_secs(5), consumer.routed(&stats_path)) .await .expect("stats announced") @@ -3870,10 +3875,10 @@ mod tests { let cluster = new_cluster(Config::default()).expect("cluster"); let all: moq_net::Patterns = [moq_net::Pattern::all()].into_iter().collect(); - let client = auth::Token::new("/", &moq_auth::Grant::new(all.clone(), all.clone())); + let client = auth::Token::new("/", &moq_auth::Grant::new(all.clone(), all.clone())).unwrap(); let mut grant = moq_auth::Grant::new(all.clone(), all); grant.peer = true; - let peer = auth::Token::new("/", &grant); + let peer = auth::Token::new("/", &grant).unwrap(); let _ingest = cluster .publisher(&client) @@ -3906,12 +3911,12 @@ mod tests { let cluster = new_cluster(Config::default()).expect("cluster"); let all: moq_net::Patterns = [moq_net::Pattern::all()].into_iter().collect(); - let client = auth::Token::new("/", &moq_auth::Grant::new(all.clone(), all.clone())); + let client = auth::Token::new("/", &moq_auth::Grant::new(all.clone(), all.clone())).unwrap(); let mut grant = moq_auth::Grant::new(all.clone(), all); grant.peer = true; - let peer = auth::Token::new("/", &grant); + let peer = auth::Token::new("/", &grant).unwrap(); grant.upstream = true; - let core = auth::Token::new("/", &grant); + let core = auth::Token::new("/", &grant).unwrap(); let _ingest = cluster .publisher(&client) diff --git a/rs/moq-relay/src/internal.rs b/rs/moq-relay/src/internal.rs index 2b9cc231a77..7c88c8420a8 100644 --- a/rs/moq-relay/src/internal.rs +++ b/rs/moq-relay/src/internal.rs @@ -1232,7 +1232,11 @@ mod tests { fn label_values_are_escaped() { let stats = moq_net::stats::Registry::new(Default::default()); let tier = "a\\b\"} 9\nmoq_relay_requests_total{tier=\"x"; - drop(stats.tier(moq_net::stats::Tier::new(tier)).request("root")); + drop( + stats + .tier(moq_net::stats::Tier::new(moq_net::Path::from_static(tier))) + .request(moq_net::Path::from_static("root")), + ); let body = render_metrics(&stats.snapshot(), &[], &[]); assert!( body.contains(r#"moq_relay_requests_total{tier="a\\b\"} 9\nmoq_relay_requests_total{tier=\"x"} 1"#), @@ -1248,7 +1252,9 @@ mod tests { #[test] fn request_metrics_never_open_a_session() { let stats = moq_net::stats::Registry::new(Default::default()); - let request = stats.tier(moq_net::stats::Tier::new("http")).request("root"); + let request = stats + .tier(moq_net::stats::Tier::new(moq_net::Path::from_static("http"))) + .request(moq_net::Path::from_static("root")); drop(request); let body = render_metrics(&stats.snapshot(), &[], &[]); assert!(body.contains("# TYPE moq_relay_requests_total counter"), "{body}"); @@ -1267,7 +1273,9 @@ mod tests { fn wire_metrics_include_internal_sessions_and_keep_final_totals() { let registry = moq_net::stats::Registry::new(Default::default()); for tier in ["internal", "edge"] { - let session = registry.tier(moq_net::stats::Tier::new(tier)).session(""); + let session = registry + .tier(moq_net::stats::Tier::new(moq_net::Path::from_static(tier))) + .session(moq_net::Path::empty()); let source = moq_net::transport::Wire::new(); let guard = session.wire(source.clone()); source.set_sent_bytes(123); @@ -1292,8 +1300,11 @@ mod tests { #[tokio::test] async fn announce_metrics_keep_retired_totals() { let stats = moq_net::stats::Registry::new(Default::default()); - let producer = - moq_tokio::origin::spawn().with_stats(stats.tier(moq_net::stats::Tier::new("edge")).session("root")); + let producer = moq_tokio::origin::spawn().with_stats( + stats + .tier(moq_net::stats::Tier::new(moq_net::Path::from_static("edge"))) + .session(moq_net::Path::from_static("root")), + ); let broadcast = producer.create_broadcast("demo/retired").unwrap(); broadcast.announce(Default::default()).unwrap(); @@ -1336,7 +1347,7 @@ mod tests { // Default-tier egress: an untagged local publisher writes, a tagged egress // consumer reads it out, so publisher `bytes` advance on the default tier. - let default_ctx = stats.tier(Tier::default()).session("acme"); + let default_ctx = stats.tier(Tier::default()).session(moq_net::Path::from_static("acme")); let pub_origin = moq_tokio::origin::spawn(); let egress = pub_origin.consume().with_stats(default_ctx.clone()); let mut announced = egress.announced(); @@ -1346,7 +1357,9 @@ mod tests { // Named-tier ingress: a tagged ingress producer writes, so subscriber // `bytes` advance on the regional tier. - let regional_ctx = stats.tier(Tier::new("region/sjc")).session("peer"); + let regional_ctx = stats + .tier(Tier::new(moq_net::Path::from_static("region/sjc"))) + .session(moq_net::Path::from_static("peer")); let sub_origin = moq_tokio::origin::spawn().with_stats(regional_ctx.clone()); let sub_source = sub_origin.create_broadcast("demo/x").unwrap(); sub_source.announce(Default::default()).unwrap(); @@ -1359,10 +1372,7 @@ mod tests { // egress bytes out of the default-tier broadcast. let (update, active) = next_update(&mut announced).await.unwrap(); assert!(active); - let bc = egress - .request_broadcast(moq_net::Path::new(update.prefix.as_str()), None) - .await - .unwrap(); + let bc = egress.request_broadcast(update.prefix.clone(), None).await.unwrap(); let mut egress_sub = bc.track("video").unwrap().subscribe(None).await.unwrap(); { let mut group = pub_track.append_group().unwrap(); diff --git a/rs/moq-relay/src/lib.rs b/rs/moq-relay/src/lib.rs index 278760ac7d5..b369aec0fb8 100644 --- a/rs/moq-relay/src/lib.rs +++ b/rs/moq-relay/src/lib.rs @@ -55,8 +55,11 @@ pub const DEFAULT_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from /// Resolve an optional stats tier label. An absent or empty label selects the /// default unprefixed tier. -fn configured_tier(label: Option) -> moq_net::stats::Tier { - label.map(moq_net::stats::Tier::new).unwrap_or_default() +fn configured_tier(label: Option) -> Result { + match label { + Some(label) => Ok(moq_net::stats::Tier::new(moq_net::Path::new(&label)?)), + None => Ok(Default::default()), + } } pub use config::*; diff --git a/rs/moq-relay/src/refusals.rs b/rs/moq-relay/src/refusals.rs index 52debeb4ccb..dd9a48025d2 100644 --- a/rs/moq-relay/src/refusals.rs +++ b/rs/moq-relay/src/refusals.rs @@ -2,7 +2,10 @@ use crate::auth; pub(crate) use moq_net::stats::RefusalKind; -use moq_net::stats::{Registry, Tier}; +use moq_net::{ + Path, + stats::{Registry, Tier}, +}; impl From<&auth::Error> for RefusalKind { // Exhaustive on purpose: a new `auth::Error` variant must pick its reason here. @@ -39,13 +42,19 @@ impl Refusals { } } pub(crate) fn record(&self, reason: RefusalKind) { - self.0.tier(Tier::default()).refuse("", reason); + self.0.tier(Tier::default()).refuse(Path::empty(), reason); } pub(crate) fn record_auth(&self, err: &auth::Error) { if let auth::Error::Refused(refusal) = err { - self.0 - .tier(refusal.tier.clone().map(Tier::new).unwrap_or_default()) - .refuse(refusal.root.as_deref().unwrap_or(""), err.into()); + // A refusal only counts: one naming a tier or root too long for a path counts + // under the default instead. + let tier = crate::configured_tier(refusal.tier.clone()).unwrap_or_default(); + let root = refusal + .root + .as_deref() + .and_then(|root| Path::new(root).ok()) + .unwrap_or_default(); + self.0.tier(tier).refuse(root, err.into()); } else { self.record(err.into()); } diff --git a/rs/moq-relay/src/stats.rs b/rs/moq-relay/src/stats.rs index 30615688aaf..c07dc46aa0d 100644 --- a/rs/moq-relay/src/stats.rs +++ b/rs/moq-relay/src/stats.rs @@ -6,7 +6,8 @@ use std::time::Duration; -use moq_net::PathOwned; +use anyhow::Context; +use moq_net::Path; use moq_net::origin; use serde::{Deserialize, Serialize}; @@ -122,13 +123,18 @@ impl Config { if !self.enabled { return Ok(moq_stats::Producer::new(moq_stats::produce::Config::new())); } - let prefix = self.prefix.clone(); + let prefix = Path::new(&self.prefix).context("invalid stats.prefix")?; let interval = self.interval(); anyhow::ensure!(interval != Some(Duration::ZERO), "stats.interval must be non-zero"); - let node = self.node.clone().map(PathOwned::from); + let node = self + .node + .as_deref() + .map(Path::new) + .transpose() + .context("invalid stats.node")?; let depth = self.depth; let linger = self.linger(); - tracing::info!(prefix, ?interval, node = ?node, depth, ?linger, "stats publishing enabled"); + tracing::info!(%prefix, ?interval, node = ?node, depth, ?linger, "stats publishing enabled"); let mut config = moq_stats::produce::Config::new() .with_origin(origin) .with_prefix(prefix) diff --git a/rs/moq-relay/src/web.rs b/rs/moq-relay/src/web.rs index 20fd9e189a0..92d78a7961c 100644 --- a/rs/moq-relay/src/web.rs +++ b/rs/moq-relay/src/web.rs @@ -1440,7 +1440,7 @@ mod tests { assert_eq!(report.traffic.len(), 2); assert_eq!(report.sessions.len(), 2); for label in ["first", "second"] { - let tier = Tier::new(label); + let tier = Tier::new(moq_net::Path::from_static(label)); let traffic = report.traffic.iter().find(|entry| entry.tier == tier).unwrap(); assert_eq!(traffic.path.as_str(), "room"); assert_eq!( diff --git a/rs/moq-relay/tests/integration/auth_lifetime.rs b/rs/moq-relay/tests/integration/auth_lifetime.rs index df617c0b4a3..49d4d056d4f 100644 --- a/rs/moq-relay/tests/integration/auth_lifetime.rs +++ b/rs/moq-relay/tests/integration/auth_lifetime.rs @@ -594,7 +594,7 @@ async fn a_moved_tier_retags_the_live_session() { let (port, relay) = spawn_relay_with(build_auth(script.spawn().await), cluster).await; let url = room_url("tcp", port); - let moved = stats::Tier::new("moved"); + let moved = stats::Tier::new(moq_net::Path::from_static("moved")); let active = |tier: &stats::Tier| { registry .snapshot() diff --git a/rs/moq-relay/tests/integration/refusals.rs b/rs/moq-relay/tests/integration/refusals.rs index 5ec64a027f5..9db71a5a70f 100644 --- a/rs/moq-relay/tests/integration/refusals.rs +++ b/rs/moq-relay/tests/integration/refusals.rs @@ -269,8 +269,8 @@ async fn refused_only_stats_depth_one_has_root_totals_and_shared_embedder_metric .expect("refusal-only root totals"); relay .stats - .tier(Tier::new("gateway")) - .refuse("tenant", RefusalKind::Forbidden); + .tier(Tier::new(moq_net::Path::from_static("gateway"))) + .refuse(moq_net::Path::from_static("tenant"), RefusalKind::Forbidden); assert_eq!( relay.refused("forbidden").await, 1, diff --git a/rs/moq-relay/tests/integration/smoke.rs b/rs/moq-relay/tests/integration/smoke.rs index 63ad6705e24..72bf4123b7f 100644 --- a/rs/moq-relay/tests/integration/smoke.rs +++ b/rs/moq-relay/tests/integration/smoke.rs @@ -189,7 +189,7 @@ async fn relay_websocket_round_trip_uses_newest_version() { .await .expect("announcement timeout") .expect("origin closed"); - let path = moq_net::Path::new(update.prefix.as_str()).to_owned(); + let path = update.prefix.clone(); assert!(active, "expected announce, got retraction"); // Auth root for `/smoke` is "smoke"; the broadcast "test" announces underneath. assert_eq!(path.as_str(), "test"); @@ -522,7 +522,7 @@ async fn relay_websocket_root_path_upgrades() { .await .expect("announcement timeout") .expect("origin closed"); - let path = moq_net::Path::new(update.prefix.as_str()).to_owned(); + let path = update.prefix.clone(); assert!(active, "expected announce, got retraction"); assert_eq!(path.as_str(), "test"); let bc = sub_consumer @@ -660,7 +660,7 @@ async fn internal_tcp_round_trip() { .await .expect("announcement timeout") .expect("origin closed"); - let path = moq_net::Path::new(update.prefix.as_str()).to_owned(); + let path = update.prefix.clone(); assert!(active, "expected announce, got retraction"); assert_eq!(path.as_str(), "test"); let bc = sub_consumer @@ -844,7 +844,7 @@ async fn path_round_trip(version: moq_net::Version, pub_url: url::Url, sub_url: .await .expect("announcement timeout") .expect("origin closed"); - let path = moq_net::Path::new(update.prefix.as_str()).to_owned(); + let path = update.prefix.clone(); drop(track); drop(bc); diff --git a/rs/moq-room/MIGRATE.md b/rs/moq-room/MIGRATE.md index 1149a75e95a..4329ad2a0cf 100644 --- a/rs/moq-room/MIGRATE.md +++ b/rs/moq-room/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `moq_net::Path` is bounded + +`broadcast_path` returns `Result`, failing when the identity and kind exceed `moq_net::Path::MAX_BYTES`. `claims` takes `impl TryInto>` and fails with `Error::Net` on a room or identity that long. + ### `chat::PRIORITY` is signed (#6142) `chat::PRIORITY` is an `i8`, still 10. diff --git a/rs/moq-room/src/claims.rs b/rs/moq-room/src/claims.rs index 038273ab370..6b8695f6507 100644 --- a/rs/moq-room/src/claims.rs +++ b/rs/moq-room/src/claims.rs @@ -8,10 +8,14 @@ /// `root` is the room prefix, `subscribe` is `**` (everything under the room), /// and `publish` is `/**` so a participant cannot publish at anyone /// else's paths. An identity that is empty after normalization or contains `*` is -/// refused, since a wildcard would let the participant publish as someone else. -pub fn claims(room: impl moq_net::AsPath, identity: impl moq_net::AsPath) -> Result { - let room = room.as_path(); - let identity = identity.as_path(); +/// refused, since a wildcard would let the participant publish as someone else, and +/// so is a room or identity longer than [`moq_net::Path::MAX_BYTES`]. +pub fn claims( + room: impl TryInto>, + identity: impl TryInto>, +) -> Result { + let room = room.try_into().map_err(Into::::into)?; + let identity = identity.try_into().map_err(Into::::into)?; if identity.is_empty() { return Err(crate::Error::EmptyIdentity); } diff --git a/rs/moq-room/src/path.rs b/rs/moq-room/src/path.rs index 4bd2bc15ebe..3630ec5f53c 100644 --- a/rs/moq-room/src/path.rs +++ b/rs/moq-room/src/path.rs @@ -4,7 +4,7 @@ //! `alice/camera`, `alice/camera.hang`, and `guest/uuid/screen` are all valid. //! A `.hang` suffix on the kind is optional and equivalent. -use moq_net::{AsPath, Path, PathOwned}; +use moq_net::{Path, path::TooLong}; /// The two broadcasts each participant may publish. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -29,7 +29,7 @@ impl Kind { #[derive(Clone, Debug, PartialEq, Eq)] pub struct Parsed { /// Path prefix identifying the participant; may be more than one segment. - pub identity: PathOwned, + pub identity: Path, /// `camera` (camera + mic) or `screen` (screenshare). pub kind: Kind, } @@ -47,42 +47,43 @@ pub fn kind_from_segment(segment: &str) -> Option { /// Split a room-relative broadcast path into identity and kind. /// /// Returns `None` when there is no identity, or the last segment is not a kind. -pub fn parse(path: impl AsPath) -> Option { - let path = path.as_path(); +pub fn parse(path: impl Into) -> Option { + let path: Path = path.into(); let mut parts: Vec<&str> = path.parts().collect(); let last = parts.pop()?; if parts.is_empty() { return None; } let kind = kind_from_segment(last)?; - let identity = Path::new(&parts.join("/")).to_owned(); + let identity = Path::new(&parts.join("/")).expect("a prefix of a path fits"); Some(Parsed { identity, kind }) } -/// The broadcast path a participant publishes for `kind`. -pub fn broadcast_path(identity: impl AsPath, kind: Kind) -> PathOwned { - identity.as_path().join(format!("{}.hang", kind.as_str())) +/// The broadcast path a participant publishes for `kind`, failing if it is too long. +pub fn broadcast_path(identity: impl Into, kind: Kind) -> Result { + let identity: Path = identity.into(); + identity.join(&Path::new(&format!("{}.hang", kind.as_str()))?) } #[cfg(test)] mod tests { use super::*; - fn p(s: &str) -> PathOwned { - Path::new(s).to_owned() + fn p(s: &str) -> Path { + Path::new(s).unwrap() } #[test] fn parse_splits_identity_and_kind() { assert_eq!( - parse(Path::new("alice/camera")), + parse(Path::from_static("alice/camera")), Some(Parsed { identity: p("alice"), kind: Kind::Camera }) ); assert_eq!( - parse(Path::new("alice/screen")), + parse(Path::from_static("alice/screen")), Some(Parsed { identity: p("alice"), kind: Kind::Screen @@ -93,11 +94,11 @@ mod tests { #[test] fn parse_accepts_hang_suffix() { assert_eq!( - parse(Path::new("alice/camera.hang")).map(|p| p.kind), + parse(Path::from_static("alice/camera.hang")).map(|p| p.kind), Some(Kind::Camera) ); assert_eq!( - parse(Path::new("alice/screen.hang")).map(|p| p.kind), + parse(Path::from_static("alice/screen.hang")).map(|p| p.kind), Some(Kind::Screen) ); } @@ -105,7 +106,7 @@ mod tests { #[test] fn parse_keeps_multi_segment_identity() { assert_eq!( - parse(Path::new("guest/uuid/camera")), + parse(Path::from_static("guest/uuid/camera")), Some(Parsed { identity: p("guest/uuid"), kind: Kind::Camera @@ -115,17 +116,20 @@ mod tests { #[test] fn parse_rejects_unknown() { - assert_eq!(parse(Path::new("alice")), None); - assert_eq!(parse(Path::new("alice/chat")), None); - assert_eq!(parse(Path::new("camera")), None); + assert_eq!(parse(Path::from_static("alice")), None); + assert_eq!(parse(Path::from_static("alice/chat")), None); + assert_eq!(parse(Path::from_static("camera")), None); assert_eq!(parse(Path::empty()), None); } #[test] fn broadcast_path_joins_with_hang_suffix() { - assert_eq!(broadcast_path(p("alice"), Kind::Camera).as_str(), "alice/camera.hang"); assert_eq!( - broadcast_path(p("guest/uuid"), Kind::Screen).as_str(), + broadcast_path(p("alice"), Kind::Camera).unwrap().as_str(), + "alice/camera.hang" + ); + assert_eq!( + broadcast_path(p("guest/uuid"), Kind::Screen).unwrap().as_str(), "guest/uuid/screen.hang" ); } diff --git a/rs/moq-room/src/room.rs b/rs/moq-room/src/room.rs index 41217cee014..c2d7ca1486a 100644 --- a/rs/moq-room/src/room.rs +++ b/rs/moq-room/src/room.rs @@ -3,7 +3,7 @@ use std::task::{Poll, ready}; use moq_net::{ - PathOwned, announce, broadcast, + Path, announce, broadcast, origin::{self, Requesting}, }; @@ -13,11 +13,11 @@ use crate::path::{Kind, parse}; #[derive(Clone)] pub struct Event { /// Participant identity (everything before `camera`/`screen`). - pub identity: PathOwned, + pub identity: Path, /// `camera` or `screen`. pub kind: Kind, /// Broadcast path relative to the room prefix. - pub path: PathOwned, + pub path: Path, /// The live broadcast, or `None` when it went offline. pub broadcast: Option, } @@ -35,9 +35,9 @@ impl std::fmt::Debug for Event { /// An in-flight request for the broadcast an active announcement named. struct Inflight { - identity: PathOwned, + identity: Path, kind: Kind, - path: PathOwned, + path: Path, request: Requesting, } @@ -48,7 +48,7 @@ struct Inflight { pub struct Room { origin: origin::Consumer, announced: announce::Consumer, - local: Option, + local: Option, inflight: Option, } @@ -62,7 +62,7 @@ impl std::fmt::Debug for Room { impl Room { /// Watch announcements on `origin`, skipping `local` when set. - pub fn new(origin: &origin::Consumer, local: Option) -> Self { + pub fn new(origin: &origin::Consumer, local: Option) -> Self { Self { announced: origin.announced(), origin: origin.clone(), @@ -171,7 +171,7 @@ mod tests { #[tokio::test] async fn yields_camera_and_skips_local_and_unknown() { let origin = origin(); - let local = Path::new("alice").to_owned(); + let local = Path::from_static("alice"); let mut room = Room::new(&origin.consume(), Some(local)); let _alice = publish(&origin, "alice/camera"); diff --git a/rs/moq-rtc/MIGRATE.md b/rs/moq-rtc/MIGRATE.md index fb000796f59..6abfe78fe47 100644 --- a/rs/moq-rtc/MIGRATE.md +++ b/rs/moq-rtc/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `moq_net::Path` has no lifetime + +`Client::publish` and `client::whip::dial` take `impl TryInto>`, so `&str`, `String`, and `&Path` still work. `server::{whip, whep}::accept` take `impl Into`: pass a `Path`, from `moq_net::Path::new(s)?` or `moq_net::Path::from_static`. See moq-net's migration for the rest. + ### Client calls run the session `Client::publish` and `Client::subscribe` resolve once the session ends, instead of once it negotiates. diff --git a/rs/moq-rtc/src/client/mod.rs b/rs/moq-rtc/src/client/mod.rs index 390d957c23b..6b8a1e735e8 100644 --- a/rs/moq-rtc/src/client/mod.rs +++ b/rs/moq-rtc/src/client/mod.rs @@ -84,7 +84,7 @@ impl Client { &self, url: Url, origin: moq_net::origin::Consumer, - path: impl moq_net::AsPath, + path: impl TryInto>, ) -> crate::Result<()> { whip::dial(self, url, origin, path).await } diff --git a/rs/moq-rtc/src/client/whip.rs b/rs/moq-rtc/src/client/whip.rs index 021e672515c..8ae87b9a814 100644 --- a/rs/moq-rtc/src/client/whip.rs +++ b/rs/moq-rtc/src/client/whip.rs @@ -21,7 +21,7 @@ pub(crate) async fn dial( client: &Client, url: Url, origin: moq_net::origin::Consumer, - path: impl moq_net::AsPath, + path: impl TryInto>, ) -> Result<()> { let broadcast = origin.request_broadcast(path, None).await?; let source = EgressSource::new(moq_mux::Source::new(origin, broadcast)).await?; diff --git a/rs/moq-rtc/src/egress.rs b/rs/moq-rtc/src/egress.rs index bcacd1cbdb6..7806aab986e 100644 --- a/rs/moq-rtc/src/egress.rs +++ b/rs/moq-rtc/src/egress.rs @@ -213,7 +213,7 @@ async fn follow_catalog(mut consumer: moq_mux::catalog::Consumer<()>, tx: watch: } } -fn valid_reference(source: &moq_mux::Source, broadcast: Option<&moq_net::path::Relative<'_>>) -> bool { +fn valid_reference(source: &moq_mux::Source, broadcast: Option<&moq_net::path::Relative>) -> bool { source.resolve_reference(broadcast).is_some() } @@ -338,7 +338,7 @@ fn pick_audio<'a>( /// must be, and the track's name there. A catalog update that changes it moves the rendition. #[derive(Clone, Debug, PartialEq, Eq)] struct Target { - path: Option, + path: Option, epoch: Option, track: String, } @@ -626,7 +626,7 @@ mod tests { let mut catalog = Catalog::default(); let mut escaped_audio = AudioConfig::new(AudioCodec::Opus, 48_000, 2); - escaped_audio.broadcast = Some(Relative::new("../../source").to_owned()); + escaped_audio.broadcast = Some(Relative::new("../../source").unwrap()); catalog.audio.renditions.insert("opus".to_string(), escaped_audio); let mut escaped_video = VideoConfig::new(H264 { @@ -635,11 +635,11 @@ mod tests { level: 0x1e, inline: false, }); - escaped_video.broadcast = Some(Relative::new("../../source").to_owned()); + escaped_video.broadcast = Some(Relative::new("../../source").unwrap()); catalog.video.renditions.insert("h264".to_string(), escaped_video); let mut valid_video = VideoConfig::new(VideoCodec::VP8); - valid_video.broadcast = Some(Relative::new("./source").to_owned()); + valid_video.broadcast = Some(Relative::new("./source").unwrap()); catalog.video.renditions.insert("vp8".to_string(), valid_video); let (writes_tx, writes_rx) = mpsc::channel(1); @@ -767,7 +767,7 @@ mod tests { // A higher-bitrate Opus this source cannot reach loses to the best one it can. let mut escaped = opus(Some(512_000)); - escaped.broadcast = Some(Relative::new("../../source").to_owned()); + escaped.broadcast = Some(Relative::new("../../source").unwrap()); catalog.audio.renditions.insert("d".to_string(), escaped); let (name, _) = pick_audio(&source, &catalog).unwrap(); assert_eq!(name, "c"); @@ -919,7 +919,7 @@ mod tests { let mut config = VideoConfig::new(VideoCodec::VP8); config.container = hang::catalog::Container::Legacy; config.track = Some(track.to_string()); - config.broadcast = broadcast.map(|broadcast| Relative::new(broadcast).into_owned()); + config.broadcast = broadcast.map(|rel| Relative::new(rel).unwrap()); catalog .modify() .unwrap() @@ -1249,7 +1249,7 @@ mod tests { config.container = hang::catalog::Container::Legacy; config.coded_width = Some(1920); config.coded_height = Some(1080); - config.broadcast = Some(Relative::new("./missing").to_owned()); + config.broadcast = Some(Relative::new("./missing").unwrap()); catalog .modify() .unwrap() diff --git a/rs/moq-rtc/src/lib.rs b/rs/moq-rtc/src/lib.rs index 367f3bf4d67..6844e399e68 100644 --- a/rs/moq-rtc/src/lib.rs +++ b/rs/moq-rtc/src/lib.rs @@ -205,7 +205,9 @@ mod tests { let origin = whep_origin.clone(); let sessions = sessions_tx.clone(); async move { - let response = server::whep::accept(&server, &origin, "source", &offer).await.unwrap(); + let response = server::whep::accept(&server, &origin, moq_net::Path::from_static("source"), &offer) + .await + .unwrap(); let answer = response.answer.clone(); sessions.send(tokio::spawn(response.run())).await.unwrap(); answer @@ -265,9 +267,10 @@ mod tests { let server = server.clone(); let origin = whip_origin.clone(); async move { - let response = server::whip::accept(&server, &origin, "ingested", &offer) - .await - .unwrap(); + let response = + server::whip::accept(&server, &origin, moq_net::Path::from_static("ingested"), &offer) + .await + .unwrap(); let answer = response.answer.clone(); tokio::spawn(response.run()); answer @@ -355,9 +358,10 @@ mod tests { let origin = whip_origin.clone(); let sources = whip_sources.clone(); async move { - let response = server::whip::accept(&server, &origin, "ingested", &offer) - .await - .unwrap(); + let response = + server::whip::accept(&server, &origin, moq_net::Path::from_static("ingested"), &offer) + .await + .unwrap(); let answer = response.answer.clone(); let id = response.resource_id.clone(); let task = tokio::spawn(response.run()); @@ -373,9 +377,10 @@ mod tests { let origin = whep_origin.clone(); let sources = sources_tx.clone(); async move { - let response = server::whep::accept(&server, &origin, "ingested", &offer) - .await - .unwrap(); + let response = + server::whep::accept(&server, &origin, moq_net::Path::from_static("ingested"), &offer) + .await + .unwrap(); let answer = response.answer.clone(); let id = response.resource_id.clone(); let task = tokio::spawn(response.run()); diff --git a/rs/moq-rtc/src/server/whep.rs b/rs/moq-rtc/src/server/whep.rs index e13472808f6..6e04025e8ce 100644 --- a/rs/moq-rtc/src/server/whep.rs +++ b/rs/moq-rtc/src/server/whep.rs @@ -97,7 +97,13 @@ async fn accept_offer( return Err(Error::InvalidSdp("expected Content-Type: application/sdp".into())); } let offer = std::str::from_utf8(&body).map_err(|err| Error::InvalidSdp(err.to_string()))?; - accept(server, subscriber, path, offer).await + accept( + server, + subscriber, + moq_net::Path::new(path).map_err(moq_net::Error::from)?, + offer, + ) + .await } #[cfg(feature = "server")] @@ -132,12 +138,12 @@ async fn delete(State(state): State, Path(path): Path) -> S pub async fn accept( server: &Server, subscriber: &moq_net::origin::Consumer, - broadcast: impl moq_net::AsPath, + broadcast: impl Into, offer: &str, ) -> Result { let offer = sdp::parse_offer(offer)?; - let source = resolve(subscriber, broadcast.as_path()).await?; + let source = resolve(subscriber, broadcast.into()).await?; let codecs = source.catalog_codecs(); if codecs.is_empty() { return Err(Error::NoRenditions); @@ -199,7 +205,7 @@ fn is_sdp(headers: &HeaderMap) -> bool { /// Both waits share one bound, or an unannounced broadcast, or an announced-but-catalog-less /// one, would park the handler forever. No route by the deadline is /// [`moq_net::Error::Unroutable`], so the WHEP client sees a 404 and retries (typical). -async fn resolve(subscriber: &moq_net::origin::Consumer, path: moq_net::Path<'_>) -> Result { +async fn resolve(subscriber: &moq_net::origin::Consumer, path: moq_net::Path) -> Result { let deadline = moq_clock::now() + CATALOG_TIMEOUT; let broadcast = moq_clock::timeout_at(deadline, subscriber.routed_broadcast(path)) .await @@ -300,7 +306,7 @@ mod tests { let (sub_origin, _connection, accept) = lazy_client(pub_origin).await; - let resolved = super::resolve(&sub_origin.consume(), moq_net::Path::new("cam")) + let resolved = super::resolve(&sub_origin.consume(), moq_net::Path::from_static("cam")) .await .expect("the broadcast resolves"); assert!(!resolved.catalog_codecs().is_empty()); diff --git a/rs/moq-rtc/src/server/whip.rs b/rs/moq-rtc/src/server/whip.rs index 4bd691f66fb..3df57a7cbf9 100644 --- a/rs/moq-rtc/src/server/whip.rs +++ b/rs/moq-rtc/src/server/whip.rs @@ -81,7 +81,13 @@ async fn accept_offer( return Err(Error::InvalidSdp("expected Content-Type: application/sdp".into())); } let offer = std::str::from_utf8(&body).map_err(|err| Error::InvalidSdp(err.to_string()))?; - accept(server, publisher, path, offer).await + accept( + server, + publisher, + moq_net::Path::new(path).map_err(moq_net::Error::from)?, + offer, + ) + .await } #[cfg(feature = "server")] @@ -114,11 +120,11 @@ async fn delete(State(state): State, Path(path): Path) -> S pub async fn accept( server: &Server, publisher: &moq_net::origin::Producer, - broadcast: impl moq_net::AsPath, + broadcast: impl Into, offer: &str, ) -> Result { let offer = sdp::parse_offer(offer)?; - let broadcast = broadcast.as_path(); + let broadcast: moq_net::Path = broadcast.into(); // Create the broadcast hidden up front, so an out-of-scope offer is refused before // any mux work. It announces only once negotiation succeeds (below). @@ -222,7 +228,9 @@ mod tests { let server = Server::new(crate::server::Config::default()); let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); - let session = accept(&server, &origin, "live/cam0", &offer()).await.unwrap(); + let session = accept(&server, &origin, moq_net::Path::from_static("live/cam0"), &offer()) + .await + .unwrap(); let broadcast = consumer.request_broadcast("live/cam0", None).await.unwrap(); let mut catalog = broadcast .track(hang::Catalog::DEFAULT_NAME) @@ -251,7 +259,9 @@ mod tests { let server = Server::new(crate::server::Config::default()); let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); - let _stale_session = accept(&server, &origin, "live/cam0", &offer()).await.unwrap(); + let _stale_session = accept(&server, &origin, moq_net::Path::from_static("live/cam0"), &offer()) + .await + .unwrap(); let stale = consumer.request_broadcast("live/cam0", None).await.unwrap(); let mut catalog = stale .track(hang::Catalog::DEFAULT_NAME) @@ -260,7 +270,9 @@ mod tests { .await .unwrap(); - let _fresh_session = accept(&server, &origin, "live/cam0", &offer()).await.unwrap(); + let _fresh_session = accept(&server, &origin, moq_net::Path::from_static("live/cam0"), &offer()) + .await + .unwrap(); // The session binds a real UDP socket, so this runs on the wall clock. let stale_viewer = tokio::time::timeout(std::time::Duration::from_millis(200), async { loop { diff --git a/rs/moq-rtmp/MIGRATE.md b/rs/moq-rtmp/MIGRATE.md index 26b5073d7b6..c5d24fb52eb 100644 --- a/rs/moq-rtmp/MIGRATE.md +++ b/rs/moq-rtmp/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `moq_net::Path` has no lifetime + +Entry points that took `impl moq_net::AsPath` take `impl Into`: pass a `Path` or `&Path`, from `moq_net::Path::new(s)?` or `moq_net::Path::from_static`, since a string can be too long. See moq-net's migration for the rest. + ### `run` and `Config` are removed `run`, `Config`, `ActivePaths`, and `PathGuard` are gone. diff --git a/rs/moq-rtmp/src/dial.rs b/rs/moq-rtmp/src/dial.rs index 2296f8cf436..fe0d20c934b 100644 --- a/rs/moq-rtmp/src/dial.rs +++ b/rs/moq-rtmp/src/dial.rs @@ -226,9 +226,9 @@ impl Client { mut self, stream_key: &str, origin: origin::Consumer, - path: impl moq_net::AsPath, + path: impl Into, ) -> Result<()> { - let path = path.as_path(); + let path: moq_net::Path = path.into(); let request = self .session .request_publishing(stream_key.to_string(), PublishRequestType::Live) @@ -313,8 +313,13 @@ impl Client { /// ends cleanly: the finished broadcast stays up for the [linger](Self::with_linger), and /// the future resolves after that. Dropping the future ends the linger early. A /// connection that drops first closes the broadcast at once. - pub async fn pull(mut self, stream_key: &str, origin: &origin::Producer, path: impl moq_net::AsPath) -> Result<()> { - let path = path.as_path(); + pub async fn pull( + mut self, + stream_key: &str, + origin: &origin::Producer, + path: impl Into, + ) -> Result<()> { + let path: moq_net::Path = path.into(); let request = self .session .request_playback(stream_key.to_string()) @@ -726,20 +731,28 @@ mod tests { let ingest = downstream.clone(); let server = tokio::spawn(async move { match accept_stream(server_io, peer).await.unwrap().unwrap() { - Request::Publish(publish) => { - publish.with_linger(linger).accept(&ingest, "live/cam0").await.unwrap() - } - Request::Play(play) => play.accept(&served, "live/cam0").await.unwrap(), + Request::Publish(publish) => publish + .with_linger(linger) + .accept(&ingest, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(), + Request::Play(play) => play + .accept(&served, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(), } }); let exported = upstream.consume(); let client = tokio::spawn(async move { let client = Client::with_stream(client_io, "live").await.unwrap(); match push { - true => client.publish("cam0", exported, "live/cam0").await.unwrap(), + true => client + .publish("cam0", exported, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(), false => client .with_linger(linger) - .pull("cam0", &pulled, "live/cam0") + .pull("cam0", &pulled, moq_net::Path::from_static("live/cam0")) .await .unwrap(), } @@ -815,12 +828,14 @@ mod tests { else { panic!("expected a publish request"); }; - let _ = publish.accept(&ingest, "live/cam0").await; + let _ = publish.accept(&ingest, moq_net::Path::from_static("live/cam0")).await; }); let exported = upstream.consume(); let pushed = tokio::spawn(async move { let client = Client::with_stream(client_io, "live").await.unwrap(); - client.publish("cam0", exported, "live/cam0").await + client + .publish("cam0", exported, moq_net::Path::from_static("live/cam0")) + .await }); // Read what reaches the remote back out as FLV. @@ -902,7 +917,9 @@ mod tests { let Request::Play(play) = request else { panic!("expected a play request"); }; - play.accept(&consumer, "live/cam0").await.unwrap(); + play.accept(&consumer, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(); }); // Client: dial, connect(`live`), play(`cam0`), republish into our own origin. @@ -911,7 +928,10 @@ mod tests { let pull_origin = client_origin.clone(); let pull = tokio::spawn(async move { let client = Client::connect(addr, "live").await.unwrap(); - client.pull("cam0", &pull_origin, "pulled/cam0").await.unwrap(); + client + .pull("cam0", &pull_origin, moq_net::Path::from_static("pulled/cam0")) + .await + .unwrap(); }); // The republished broadcast should show up in the client's origin. diff --git a/rs/moq-rtmp/src/server.rs b/rs/moq-rtmp/src/server.rs index 12c6af25784..5439b722944 100644 --- a/rs/moq-rtmp/src/server.rs +++ b/rs/moq-rtmp/src/server.rs @@ -630,8 +630,8 @@ impl Publish { /// stays up for the [linger](Self::with_linger), and the future resolves after that. /// Dropping the future ends the linger early. A connection that drops without /// unpublishing closes the broadcast at once. - pub async fn accept(mut self, origin: &origin::Producer, path: impl moq_net::AsPath) -> Result<()> { - let path = path.as_path(); + pub async fn accept(mut self, origin: &origin::Producer, path: impl Into) -> Result<()> { + let path: moq_net::Path = path.into(); // Reserve the broadcast path before telling the client the publish succeeded: // if the origin refuses `path`, reject cleanly instead of accepting and then // dropping the connection a moment later. @@ -789,8 +789,8 @@ impl Play { /// before the publisher), cancelling cleanly if the viewer disconnects first. /// This future resolves when playback ends, so callers usually run it on its /// own task. - pub async fn accept(mut self, origin: &origin::Consumer, path: impl moq_net::AsPath) -> Result<()> { - let path = path.as_path(); + pub async fn accept(mut self, origin: &origin::Consumer, path: impl Into) -> Result<()> { + let path: moq_net::Path = path.into(); // Wait for the broadcast before telling the client playback started. Feed the // client's bytes through the session (not discard them) so its deserializer // stays in sync for everything `play_pump` parses next. @@ -1740,7 +1740,11 @@ mod tests { let Some(Request::Publish(publish)) = accept_stream(stream, peer).await.unwrap() else { panic!("expected a publish request"); }; - publish.with_linger(linger).accept(&origin, "live/cam0").await.unwrap(); + publish + .with_linger(linger) + .accept(&origin, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(); } /// An unpublish is a clean end: the finished broadcast stays up for the linger, then @@ -2137,7 +2141,9 @@ mod tests { let Request::Play(play) = request else { panic!("expected a play request"); }; - play.accept(&consumer, "live/cam0").await.unwrap(); + play.accept(&consumer, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(); }); let stream = TcpStream::connect(addr).await.unwrap(); @@ -2203,7 +2209,7 @@ mod tests { let Some(Request::Play(play)) = server.accept().await else { panic!("expected a play request"); }; - play.accept(&play_origin, "live/cam0").await + play.accept(&play_origin, moq_net::Path::from_static("live/cam0")).await }); let (tx, mut media) = tokio::sync::mpsc::unbounded_channel(); let client = tokio::spawn(play_client(TcpStream::connect(addr).await.unwrap(), 0, &[], tx)); @@ -2271,7 +2277,9 @@ mod tests { let Request::Play(play) = request else { panic!("expected a play request"); }; - play.accept(&consumer, "live/cam0").await.unwrap(); + play.accept(&consumer, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(); }); let stream = TcpStream::connect(addr).await.unwrap(); @@ -2546,7 +2554,9 @@ mod tests { let Request::Play(play) = request else { panic!("expected a play request"); }; - play.accept(&consumer, "live/cam0").await.unwrap(); + play.accept(&consumer, moq_net::Path::from_static("live/cam0")) + .await + .unwrap(); }); // The player advertises multitrack; collect the four video messages (two @@ -2602,7 +2612,9 @@ mod tests { tokio::time::pause(); let server_task = tokio::spawn(async move { - play.accept(&consumer, "live/missing").await.unwrap(); + play.accept(&consumer, moq_net::Path::from_static("live/missing")) + .await + .unwrap(); }); tokio::task::yield_now().await; @@ -2664,7 +2676,7 @@ mod tests { panic!("expected a publish request"); }; let origin = origin.clone(); - tokio::spawn(async move { publish.accept(&origin, "live/cam0").await }); + tokio::spawn(async move { publish.accept(&origin, moq_net::Path::from_static("live/cam0")).await }); } }); diff --git a/rs/moq-scene/src/source.rs b/rs/moq-scene/src/source.rs index 77a5b3155c8..b1e8c46c9c8 100644 --- a/rs/moq-scene/src/source.rs +++ b/rs/moq-scene/src/source.rs @@ -692,7 +692,7 @@ mod tests { let mut broadcast = publication.broadcast().clone(); let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut config = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2); - config.broadcast = Some("../other".into()); + config.broadcast = Some(moq_net::path::Relative::new("../other").unwrap()); catalog .mutate(|catalog| { catalog.audio.renditions.insert("audio".into(), config); diff --git a/rs/moq-srt/MIGRATE.md b/rs/moq-srt/MIGRATE.md index ff3f70c6dfb..342bc719f31 100644 --- a/rs/moq-srt/MIGRATE.md +++ b/rs/moq-srt/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `moq_net::Path` has no lifetime + +Entry points that took `impl moq_net::AsPath` take `impl Into`: pass a `Path` or `&Path`, from `moq_net::Path::new(s)?` or `moq_net::Path::from_static`, since a string can be too long. See moq-net's migration for the rest. + ### `with_linger` keeps an ingest up; `with_follow` follows an egress | Before | After | diff --git a/rs/moq-srt/src/dial.rs b/rs/moq-srt/src/dial.rs index cc84e22a2c5..99f8339a028 100644 --- a/rs/moq-srt/src/dial.rs +++ b/rs/moq-srt/src/dial.rs @@ -151,8 +151,8 @@ impl Client { } /// Push a MoQ broadcast out to the remote as MPEG-TS until the broadcast ends. - pub async fn publish(&self, origin: &origin::Consumer, path: impl moq_net::AsPath) -> Result<()> { - let path = path.as_path(); + pub async fn publish(&self, origin: &origin::Consumer, path: impl Into) -> Result<()> { + let path: moq_net::Path = path.into(); let socket = self.call(Mode::Publish).await?; let options = crate::ts::Options { latency: self.max_delay, @@ -166,8 +166,8 @@ impl Client { /// /// The finished broadcast then stays up for the [linger](Self::with_linger), as /// [`Publish::accept`](crate::Publish::accept) describes, and the future resolves after that. - pub async fn pull(&self, origin: &origin::Producer, path: impl moq_net::AsPath) -> Result<()> { - let path = path.as_path(); + pub async fn pull(&self, origin: &origin::Producer, path: impl Into) -> Result<()> { + let path: moq_net::Path = path.into(); let socket = self.call(Mode::Request).await?; let ingest = crate::server::Ingest { config: moq_mux::catalog::Config::default() @@ -265,7 +265,7 @@ mod tests { // Runs until the caller disconnects, then closes at once. publish .with_linger(Duration::ZERO) - .accept(&origin, "ingested/cam0") + .accept(&origin, moq_net::Path::from_static("ingested/cam0")) .await .ok(); } @@ -306,7 +306,10 @@ mod tests { let resource = request.resource().to_string(); let is_subscribe = matches!(request, Request::Subscribe(_)); if let Request::Subscribe(subscribe) = request { - subscribe.accept(&consumer, "live/cam0").await.ok(); + subscribe + .accept(&consumer, moq_net::Path::from_static("live/cam0")) + .await + .ok(); } (resource, is_subscribe) }); diff --git a/rs/moq-srt/src/server.rs b/rs/moq-srt/src/server.rs index 9e7f47c5471..cff326f0cd3 100644 --- a/rs/moq-srt/src/server.rs +++ b/rs/moq-srt/src/server.rs @@ -418,8 +418,8 @@ impl Publish { /// Once the stream ends, the finished broadcast stays up for the /// [linger](Self::with_linger), and the future resolves after that. Dropping the future /// ends the linger early. A socket or demux error closes the broadcast at once. - pub async fn accept(self, origin: &origin::Producer, path: impl moq_net::AsPath) -> Result<()> { - let path = path.as_path(); + pub async fn accept(self, origin: &origin::Producer, path: impl Into) -> Result<()> { + let path: moq_net::Path = path.into(); let socket = self.0.request.accept(None).await?; tracing::info!(peer = %self.0.peer, %path, "SRT publish accepted"); let config = moq_mux::catalog::Config::default() @@ -493,8 +493,8 @@ impl Subscribe { /// Waits for the broadcast to be announced (so a caller may connect before the /// publisher), cancelling cleanly if the caller disconnects first. This future /// resolves when playback ends, so callers usually run it on its own task. - pub async fn accept(self, origin: &origin::Consumer, path: impl moq_net::AsPath) -> Result<()> { - let path = path.as_path(); + pub async fn accept(self, origin: &origin::Consumer, path: impl Into) -> Result<()> { + let path: moq_net::Path = path.into(); let socket = self.0.request.accept(None).await?; tracing::info!(peer = %self.0.peer, %path, "SRT subscribe accepted"); let options = crate::ts::Options { @@ -964,7 +964,8 @@ mod tests { panic!("m=request must create a subscribe request"); }; let consumer = origin.consume(); - let egress = tokio::spawn(async move { subscribe.accept(&consumer, "marker").await }); + let egress = + tokio::spawn(async move { subscribe.accept(&consumer, moq_net::Path::from_static("marker")).await }); let mut receiver = caller.await.unwrap(); // Three seconds of program, then wait until the receiver is actually playing it @@ -1102,7 +1103,7 @@ mod tests { subscribe .with_follow(follow) .with_stitch(stitch) - .accept(&consumer, "live") + .accept(&consumer, moq_net::Path::from_static("live")) .await }); (caller.await.unwrap(), task) diff --git a/rs/moq-srt/src/ts.rs b/rs/moq-srt/src/ts.rs index 407318b5078..9424d837955 100644 --- a/rs/moq-srt/src/ts.rs +++ b/rs/moq-srt/src/ts.rs @@ -79,7 +79,11 @@ impl Publisher { // it. `ts::Programs` mints one per program likewise. let mut epoch = None; let importer = match program { - Some(Program::All) => Importer::All(Box::new(ts::Programs::new(origin.clone(), path, config))), + Some(Program::All) => Importer::All(Box::new(ts::Programs::new( + origin.clone(), + moq_net::Path::new(path).map_err(moq_net::Error::from)?, + config, + ))), Some(Program::One(_)) | None => { let route = moq_net::origin::Route::default().with_epoch(epoch.insert(moq_net::Epoch::mint()).clone()); let broadcast = origin.publish(path, route)?; diff --git a/rs/moq-stats/MIGRATE.md b/rs/moq-stats/MIGRATE.md index 9721fe39880..c6702e23dff 100644 --- a/rs/moq-stats/MIGRATE.md +++ b/rs/moq-stats/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `moq_net::Path` has no lifetime + +Functions that took `impl AsPath` take `impl Into`, so pass a `Path` rather than a string, and fields and returns that were `PathOwned` are `moq_net::Path`. See moq-net's migration for the rest. + ### `Refusal` is `RefusalKind` (#6510) The re-exported `moq_stats::Refusal` follows moq-net's rename: use `moq_stats::RefusalKind`. diff --git a/rs/moq-stats/benches/aggregate.rs b/rs/moq-stats/benches/aggregate.rs index b75b9f30c2b..4281668ad50 100644 --- a/rs/moq-stats/benches/aggregate.rs +++ b/rs/moq-stats/benches/aggregate.rs @@ -55,7 +55,7 @@ impl Node { .unwrap(); let track = source .create_track( - group_track("acme"), + group_track(moq_net::Path::from_static("acme")), moq_net::track::Info::default().with_timescale(moq_net::Timescale::MILLI), ) .unwrap(); @@ -118,7 +118,7 @@ async fn measure(live: usize, departed: usize, grace: Duration, state: State) -> let (origin, driver) = origin::Producer::new(origin::Config::default()); let driver = tokio::spawn(moq_net::time::run(driver)); let agg = aggregate::Consumer::new(origin.consume(), aggregate::Config::new().with_grace(grace)); - let mut traffic = agg.group("acme"); + let mut traffic = agg.group(moq_net::Path::from_static("acme")); // Every node contributes 1 byte per path, so the total counts nodes seen. let mut total = 0; diff --git a/rs/moq-stats/benches/producer_tick.rs b/rs/moq-stats/benches/producer_tick.rs index bb4aed2960f..48fce61d045 100644 --- a/rs/moq-stats/benches/producer_tick.rs +++ b/rs/moq-stats/benches/producer_tick.rs @@ -128,8 +128,8 @@ impl Relay { let mut sessions = Vec::with_capacity(tiers); let tier_labels: Vec<_> = (0..tiers) .map(|tier| { - let tier = stats::Tier::new(format!("t{tier}")); - let session = registry.tier(tier.clone()).session("root"); + let tier = stats::Tier::new(moq_net::Path::new(&format!("t{tier}")).unwrap()); + let session = registry.tier(tier.clone()).session(moq_net::Path::from_static("root")); let mut cursor = origin.consume().with_stats(session.clone()).announced(); while cursor.next().now_or_never().flatten().is_some() {} cursors.push(cursor); @@ -174,7 +174,12 @@ impl Relay { } for path in self.paths.iter().step_by(self.step) { for tier in &self.tiers { - moq_net::fuzz::bump_publisher_bytes(&self.registry, path.as_str(), tier, 1); + moq_net::fuzz::bump_publisher_bytes( + &self.registry, + moq_net::Path::new(path.as_str()).unwrap(), + tier, + 1, + ); } } } @@ -300,7 +305,11 @@ impl Presence { let names: Vec = (0..labels).map(|index| format!("room/{index:05}")).collect(); let sessions: Vec<_> = names .iter() - .map(|label| registry.tier(stats::Tier::default()).session(label.as_str())) + .map(|label| { + registry + .tier(stats::Tier::default()) + .session(moq_net::Path::new(label.as_str()).unwrap()) + }) .collect(); // One set of wire counters per connection, independently of held presence tracks. let wire = sessions diff --git a/rs/moq-stats/src/aggregate.rs b/rs/moq-stats/src/aggregate.rs index 5362551d7f0..ae48146fefc 100644 --- a/rs/moq-stats/src/aggregate.rs +++ b/rs/moq-stats/src/aggregate.rs @@ -15,7 +15,7 @@ use moq_clock::Instant; use moq_net::kio::{self, Pending, Waiter}; use moq_net::stats::{Presence, Traffic}; use moq_net::track::Subscribing; -use moq_net::{AsPath, Epoch, PathOwned, broadcast, origin}; +use moq_net::{Epoch, Path, broadcast, origin}; use crate::{ Encoding, Group, GroupFrame, Prefix, PrefixFrame, PresenceFrame, Result, Totals, TotalsFrame, Usage, group_track, @@ -34,7 +34,7 @@ use crate::{ pub struct Config { /// Top-level path stats are published under (default `.stats`). Must match /// the producer's prefix. - pub prefix: PathOwned, + pub prefix: Path, /// Which encoding of the totals, presence, and prefix tracks to read. /// [`Encoding::Compressed`] carries the same data for a fraction of the /// bytes. Defaults to [`Encoding::Plain`]. A group's track compresses @@ -56,7 +56,7 @@ impl Config { } /// Override the top-level prefix (default `.stats`). Must match the producer. - pub fn with_prefix(mut self, prefix: impl Into) -> Self { + pub fn with_prefix(mut self, prefix: impl Into) -> Self { self.prefix = prefix.into(); self } @@ -77,7 +77,7 @@ impl Config { impl Default for Config { fn default() -> Self { Self { - prefix: PathOwned::from(".stats"), + prefix: Path::from_static(".stats"), encoding: Encoding::Plain, // Well above reconnect times; a longer grace only costs memory. grace: Duration::from_secs(600), @@ -149,7 +149,7 @@ impl Consumer { /// A merged reader over group `group`'s track on every node broadcast that /// lists it; see [`Self::totals`]. - pub fn group(&self, group: impl AsPath) -> GroupConsumer { + pub fn group(&self, group: impl Into) -> GroupConsumer { GroupConsumer { inner: self.merged(group_track(group), Encoding::Plain), } @@ -157,7 +157,7 @@ impl Consumer { /// A merged reader over `label`'s presence track, holding it on every /// node broadcast; see [`Self::totals`]. - pub fn presence(&self, label: impl AsPath) -> PresenceConsumer { + pub fn presence(&self, label: impl Into) -> PresenceConsumer { PresenceConsumer { inner: self.merged(presence_track(label, self.config.encoding), self.config.encoding), } @@ -165,7 +165,7 @@ impl Consumer { /// A merged reader over broadcast prefix `path`'s track, holding it on /// every node broadcast; see [`Self::totals`]. - pub fn prefix(&self, path: impl AsPath) -> PrefixConsumer { + pub fn prefix(&self, path: impl Into) -> PrefixConsumer { PrefixConsumer { inner: self.merged(prefix_track(path, self.config.encoding), self.config.encoding), } @@ -740,7 +740,7 @@ struct Node { offered: bool, /// The announced path, relative to the announce cursor: what a re-resolve /// after the reader ends requests again. - path: PathOwned, + path: Path, /// The publisher instance every initial and retry resolution must serve. epoch: Option, /// The last known publisher identity, retained while route metadata vanishes. @@ -789,15 +789,15 @@ struct Merged { /// Resolves announced node paths into broadcasts. origin: origin::Consumer, announce: moq_net::announce::Consumer, - prefix: PathOwned, + prefix: Path, /// Track name subscribed on each node broadcast. name: String, config: moq_json::snapshot::consumer::Config, /// One entry per live or recently departed node broadcast, keyed by /// absolute announced path and route epoch, so each epoch is its own entry. - nodes: HashMap<(PathOwned, Option), Node>, + nodes: HashMap<(Path, Option), Node>, /// The immediately outgoing identity, including while it remains in grace. - last_announced: HashMap>, + last_announced: HashMap>, /// How long a departed node waits before folding into `retired`. grace: Duration, /// The summed last frames of every node whose grace elapsed. @@ -845,8 +845,10 @@ impl Merged { // new one starts, so its reader resolves afresh. Poll::Ready(Some(moq_net::announce::Event::Restart(update))) => { let mut ended = update.clone(); - let absolute = self.announce.absolute(&update.prefix).to_owned(); - ended.route.epoch = self.last_announced.get(&absolute).cloned().flatten(); + // An announced prefix is a path beneath the root, so rooting it fits. + let absolute = self.announce.absolute(&update.prefix).ok(); + ended.route.epoch = + absolute.and_then(|absolute| self.last_announced.get(&absolute).cloned().flatten()); changed |= self.apply_announce(ended, false); changed |= self.apply_announce(update, true); } @@ -895,7 +897,10 @@ impl Merged { /// leaving, does). fn apply_announce(&mut self, update: moq_net::announce::Announce, active: bool) -> bool { let path = update.prefix; - let absolute = self.announce.absolute(&path).to_owned(); + // An announced prefix is a path beneath the root, so rooting it fits. + let Ok(absolute) = self.announce.absolute(&path) else { + return false; + }; // Only fold node-category routes; skip sibling categories a producer // may publish under the same prefix. A route names a prefix, and node @@ -1159,7 +1164,7 @@ fn advance( } /// Start resolving `path` (relative to the announce cursor) into a broadcast. -fn resolve(origin: &origin::Consumer, path: &PathOwned, epoch: Option) -> Reader { +fn resolve(origin: &origin::Consumer, path: &Path, epoch: Option) -> Reader { let pending = origin.request_broadcast(path, epoch); let queued = pending.is_queued(); Reader::Resolving { pending, queued } @@ -1197,7 +1202,7 @@ mod tests { use std::time::Duration; - use moq_net::{PathOwned, Timestamp, announce, broadcast, origin, track}; + use moq_net::{Path, Timestamp, announce, broadcast, origin, track}; use moq_net::stats::Tier; @@ -1227,7 +1232,7 @@ mod tests { fn traffic(agg: &Consumer) -> TrafficConsumer { TrafficConsumer { - inner: agg.group("acme").inner, + inner: agg.group(moq_net::Path::from_static("acme")).inner, } } @@ -1250,7 +1255,7 @@ mod tests { fn sessions(agg: &Consumer) -> SessionsConsumer { SessionsConsumer { - inner: agg.group("acme"), + inner: agg.group(moq_net::Path::from_static("acme")), } } @@ -1287,7 +1292,7 @@ mod tests { Producer::new( produce::Config::new() .with_origin(origin.clone()) - .with_node(PathOwned::from(node.to_string())) + .with_node(Path::try_from(node.to_string()).unwrap()) .with_depth(1), ) } @@ -1307,7 +1312,10 @@ mod tests { /// `tier`/`label`, by driving a throwaway tagged broadcast. The broadcast lives /// on its own origin so it never lands on the stats origin. async fn feed(producer: &Producer, tier: Tier, label: &str, path: &str, bytes: usize) -> Feed { - let ctx = producer.registry().tier(tier).session(label); + let ctx = producer + .registry() + .tier(tier) + .session(moq_net::Path::new(label).unwrap()); let feed_origin = produce_origin(); let egress = feed_origin.consume().with_stats(ctx.clone()); @@ -1388,7 +1396,9 @@ mod tests { let path = format!(".stats/node/{node}"); let source = origin.create_broadcast(path.as_str()).expect("create broadcast"); source.announce(origin::Route::default()).expect("announce"); - let track = source.create_track(group_track(group), timed()).expect("create track"); + let track = source + .create_track(group_track(moq_net::Path::new(group).unwrap()), timed()) + .expect("create track"); let config = moq_json::snapshot::Config::default().with_deltas(false); Self { group: moq_json::snapshot::Producer::new(track.clone(), config), @@ -1580,7 +1590,7 @@ mod tests { #[test] fn outgoing_epoch_metadata_migrates_without_choosing_a_grace_entry() { let origin = produce_origin(); - let path = PathOwned::from(".stats/node/a"); + let path = Path::from_static(".stats/node/a"); let a: Epoch = "01900000-0000-7000-8000-000000000001".parse().unwrap(); let b: Epoch = "01900000-0000-7000-8000-000000000002".parse().unwrap(); let c: Epoch = "01900000-0000-7000-8000-000000000003".parse().unwrap(); @@ -1702,7 +1712,7 @@ mod tests { .announce(origin::Route::default().with_epoch(epoch_a.clone())) .unwrap(); old.publish("acme/room", 100); - let name = group_track("acme"); + let name = group_track(moq_net::Path::from_static("acme")); let mut merged = Merged::::new(origin.consume(), &Config::new(), name.clone(), Encoding::Plain); let initial = futures::executor::block_on(kio::wait(|waiter| { driver.poll(now, waiter).unwrap(); @@ -1721,7 +1731,7 @@ mod tests { .unwrap(); assert_eq!(unversioned[""].paths["acme/room"].publisher.bytes, 100); assert!(matches!( - merged.nodes[&(PathOwned::from(".stats/node/a"), None)].reader, + merged.nodes[&(Path::from_static(".stats/node/a"), None)].reader, Reader::Active(_) )); } @@ -1761,7 +1771,7 @@ mod tests { (new, front, warm, driver) }); ready.wait(); - let key = (PathOwned::from(".stats/node/a"), (!drop_epoch).then_some(epoch_a)); + let key = (Path::from_static(".stats/node/a"), (!drop_epoch).then_some(epoch_a)); let node = merged.nodes.get_mut(&key).unwrap(); advance(node, &merged.origin, &merged.config, &name, &Waiter::noop()); let paths = node.shape.paths.last.as_ref().unwrap(); @@ -2051,8 +2061,14 @@ mod tests { // group. let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 8).await; let _fb = feed(&node_b, Tier::default(), "acme", "acme/room", 8).await; - let _sa = node_a.registry().tier(Tier::default()).session("acme"); - let _sb = node_b.registry().tier(Tier::default()).session("acme"); + let _sa = node_a + .registry() + .tier(Tier::default()) + .session(moq_net::Path::from_static("acme")); + let _sb = node_b + .registry() + .tier(Tier::default()) + .session(moq_net::Path::from_static("acme")); drive_tick().await; let agg = Consumer::new(origin.consume(), Config::new()); @@ -2078,8 +2094,14 @@ mod tests { let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 8).await; let fb = feed(&node_b, Tier::default(), "acme", "acme/room", 8).await; - let _sa = node_a.registry().tier(Tier::default()).session("acme"); - let sb = node_b.registry().tier(Tier::default()).session("acme"); + let _sa = node_a + .registry() + .tier(Tier::default()) + .session(moq_net::Path::from_static("acme")); + let sb = node_b + .registry() + .tier(Tier::default()) + .session(moq_net::Path::from_static("acme")); drive_tick().await; let agg = Consumer::new(origin.consume(), Config::new()); @@ -2169,14 +2191,17 @@ mod tests { let node = Producer::new( produce::Config::new() .with_origin(origin.clone()) - .with_node(PathOwned::from("a")) + .with_node(Path::from_static("a")) .with_depth(1) .with_linger(Duration::ZERO), ); let agg = Consumer::new(origin.consume(), Config::new()); - let mut group = agg.group("acme"); + let mut group = agg.group(moq_net::Path::from_static("acme")); - let session = node.registry().tier(Tier::default()).session("acme/live"); + let session = node + .registry() + .tier(Tier::default()) + .session(moq_net::Path::from_static("acme/live")); drive_tick().await; let mut started = 0; let mut read_until = async |want: u64, active: u64| loop { @@ -2195,7 +2220,10 @@ mod tests { for _ in 0..3 { drive_tick().await; } - let _session = node.registry().tier(Tier::default()).session("acme/live"); + let _session = node + .registry() + .tier(Tier::default()) + .session(moq_net::Path::from_static("acme/live")); drive_tick().await; read_until(2, 1).await; } @@ -2263,9 +2291,9 @@ mod tests { /// A node announced at `.stats/node/a` with no epoch, ready to observe /// frames by hand. - fn hand_merged() -> (Merged>, (PathOwned, Option)) { + fn hand_merged() -> (Merged>, (Path, Option)) { let origin = produce_origin(); - let path = PathOwned::from(".stats/node/a"); + let path = Path::from_static(".stats/node/a"); let mut merged = Merged::>::new(origin.consume(), &Config::new(), "p".into(), Encoding::Plain); merged.apply_announce( moq_net::announce::Announce { @@ -2287,7 +2315,7 @@ mod tests { value.sessions_failed.transport = failed; value }; - let path = PathOwned::from(".stats/node/b"); + let path = Path::from_static(".stats/node/b"); merged.apply_announce( moq_net::announce::Announce { prefix: path.clone(), @@ -2613,7 +2641,7 @@ mod tests { drive_tick().await; let agg = Consumer::new(origin.consume(), Config::new()); - let mut presence = agg.presence("acme"); + let mut presence = agg.presence(moq_net::Path::from_static("acme")); let grew = |before: &Presence, after: &Presence| !after.regressed(before); read_until(&mut presence.inner, grew, |frame| { frame.get("").is_some_and(|p| p.active() == 1) @@ -2650,7 +2678,7 @@ mod tests { drive_tick().await; let agg = Consumer::new(origin.consume(), Config::new()); - let mut prefix = agg.prefix("acme"); + let mut prefix = agg.prefix(moq_net::Path::from_static("acme")); let grew = |before: &Prefix, after: &Prefix| !after.regressed(before); let live = |count| { move |frame: &PrefixFrame| { diff --git a/rs/moq-stats/src/consume.rs b/rs/moq-stats/src/consume.rs index 2e24af21abf..42c94813c7e 100644 --- a/rs/moq-stats/src/consume.rs +++ b/rs/moq-stats/src/consume.rs @@ -1,6 +1,6 @@ //! The consuming half: typed readers over one published stats broadcast. -use moq_net::{AsPath, broadcast}; +use moq_net::{Path, broadcast}; use crate::{ Encoding, GroupFrame, PrefixFrame, PresenceFrame, Result, TotalsFrame, group_track, prefix_track, presence_track, @@ -62,7 +62,7 @@ impl Consumer { /// Subscribe to the presence track for `label`, awaiting the /// subscription handshake. - pub async fn presence(&self, label: impl AsPath) -> Result { + pub async fn presence(&self, label: impl Into) -> Result { let name = presence_track(label, self.config.encoding); Ok(PresenceConsumer { inner: self.subscribe(&name).await?, @@ -71,7 +71,7 @@ impl Consumer { /// Subscribe to the prefix track for broadcast prefix `path`, awaiting the /// subscription handshake. - pub async fn prefix(&self, path: impl AsPath) -> Result { + pub async fn prefix(&self, path: impl Into) -> Result { let name = prefix_track(path, self.config.encoding); Ok(PrefixConsumer { inner: self.subscribe(&name).await?, @@ -82,7 +82,7 @@ impl Consumer { /// The track compresses itself, so [`Config::encoding`] does not apply. /// Fails with [`NotFound`](moq_net::Error::NotFound) unless the group is /// listed. - pub async fn group(&self, group: impl AsPath) -> Result { + pub async fn group(&self, group: impl Into) -> Result { let track = self.broadcast.track(&group_track(group))?.subscribe(None).await?; Ok(GroupConsumer { inner: moq_json::snapshot::Consumer::new(track, Default::default()), @@ -185,7 +185,7 @@ mod tests { use std::time::Duration; - use moq_net::{Consume, PathOwned, Timestamp, announce, origin, track}; + use moq_net::{Consume, Path, Timestamp, announce, origin, track}; use crate::{Encoding, Producer, Tier, produce}; @@ -196,7 +196,7 @@ mod tests { let producer = Producer::new( produce::Config::new() .with_origin(origin.clone()) - .with_node(PathOwned::from("sjc")), + .with_node(Path::from_static("sjc")), ); (producer, origin) } @@ -224,7 +224,10 @@ mod tests { } async fn feed(producer: &Producer, tier: Tier, label: &str, path: &str) -> Feed { - let ctx = producer.registry().tier(tier).session(label); + let ctx = producer + .registry() + .tier(tier) + .session(moq_net::Path::new(label).unwrap()); let feed_origin = produce_origin(); let egress = feed_origin.consume().with_stats(ctx.clone()); @@ -254,7 +257,7 @@ mod tests { assert!(active); origin .consume() - .request_broadcast(moq_net::Path::new(update.prefix.as_str()), None) + .request_broadcast(&update.prefix, None) .await .expect("resolve") } @@ -300,7 +303,10 @@ mod tests { assert_eq!(plain_frame, z_frame, "delta reconstructs the same frame"); // At depth 0 the one group is the empty one, compressed by its track. - let mut group = compressed.group("").await.expect("subscribe group"); + let mut group = compressed + .group(moq_net::Path::from_static("")) + .await + .expect("subscribe group"); let frame = group.next().await.expect("read").expect("frame"); let tier = &frame.tiers[""]; assert_eq!(tier.publisher.bytes, 50); @@ -308,7 +314,7 @@ mod tests { assert_eq!(tier.labels["acme"].active(), 1); // A presence track is produced once requested, so drive a drain. - let mut presence = std::pin::pin!(compressed.presence("acme")); + let mut presence = std::pin::pin!(compressed.presence(moq_net::Path::from_static("acme"))); assert!(futures::poll!(presence.as_mut()).is_pending()); drive_tick().await; let mut presence = presence.await.expect("subscribe presence"); @@ -316,7 +322,7 @@ mod tests { assert_eq!(frame.get("").expect("default tier").active(), 1); // So is a prefix track. - let mut prefix = std::pin::pin!(compressed.prefix("foo")); + let mut prefix = std::pin::pin!(compressed.prefix(moq_net::Path::from_static("foo"))); assert!(futures::poll!(prefix.as_mut()).is_pending()); drive_tick().await; let mut prefix = prefix.await.expect("subscribe prefix"); diff --git a/rs/moq-stats/src/lib.rs b/rs/moq-stats/src/lib.rs index 3e094a72946..7ebd9e36343 100644 --- a/rs/moq-stats/src/lib.rs +++ b/rs/moq-stats/src/lib.rs @@ -49,7 +49,7 @@ use std::collections::BTreeMap; /// can depend on this crate alone. pub use moq_net::stats::{Failure, Failures, Handle, Presence, RefusalKind, Refusals, Registry, Role, Tier, Traffic}; -use moq_net::{AsPath, Epoch, Path, PathOwned}; +use moq_net::{Epoch, Path}; /// One frame off the totals track: each tier's [`Totals`], keyed by tier label /// (`""` for the default tier). @@ -223,27 +223,27 @@ pub fn totals_track(encoding: Encoding) -> String { /// The presence track name for a stats label: `