Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d26eeaf
wip: Path loses its lifetime
kixelated Oct 25, 2026
bc20f51
wip: drop to_owned calls
kixelated Oct 25, 2026
626fabf
wip: concat without realloc, compress heads by str
kixelated Oct 25, 2026
b02cd28
wip: fmt
kixelated Oct 25, 2026
d99110b
wip: u16 offset keeps 32-bit Path at main's size
kixelated Oct 26, 2026
67fe62c
Merge origin/main
kixelated Oct 26, 2026
a13b75e
refactor(net): one copy constructor for Repr
kixelated Oct 26, 2026
1e249b5
feat(net): cap paths at 64 KiB on the wire and on an origin
kixelated Oct 26, 2026
228cc69
feat(net): share the 64 KiB path limit with @moq/net and the draft
kixelated Oct 26, 2026
48c3430
fix(net): cap paths at 16 KiB so a message carrying one still fits
kixelated Oct 26, 2026
bc3f3b6
Merge remote-tracking branch 'origin/main' into path-no-lifetime
kixelated Oct 26, 2026
cad1350
refactor(net)!: bound every Path at 16 KiB with fallible constructors
kixelated Oct 26, 2026
4c8961c
fix(net): measure a wire path before normalizing, and refuse an overl…
kixelated Oct 26, 2026
8da8e7b
Merge remote-tracking branch 'origin/main' into path-no-lifetime
kixelated Oct 26, 2026
ff6ee38
fix(net): refuse a request too long under the root on its own stream,…
kixelated Oct 26, 2026
78a9714
Merge remote-tracking branch 'origin/main' into path-no-lifetime
kixelated Oct 26, 2026
7a071fa
Merge remote-tracking branch 'origin/main' into merge-6843
kixelated Oct 26, 2026
42c2c55
fix(moq-cli): build echo_test's Echo from a parsed Relative
kixelated Oct 26, 2026
fa801da
Merge remote-tracking branch 'origin/main' into merge-6843
kixelated Oct 26, 2026
0d9cd6c
fix(js/net): refuse an announced path the interest prefix pushes past…
kixelated Oct 27, 2026
8d4c558
Merge remote-tracking branch 'origin/main' into merge-6843
kixelated Oct 27, 2026
f19e97c
Merge origin/main into path-no-lifetime
kixelated Oct 27, 2026
2ddf656
Merge origin/main into path-no-lifetime
kixelated Oct 27, 2026
e393c6c
Merge origin/main into path-no-lifetime
kixelated Oct 27, 2026
8213385
fix(stats): drop the needless borrows clippy denies in the producer_t…
kixelated Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions drafts/draft-lcurley-moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions js/net/api/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -524,6 +526,7 @@ declare namespace Path {
Valid,
Relative,
MAX_PARTS,
MAX_BYTES,
compareSpecificity,
InvalidPattern,
Pattern,
Expand Down
11 changes: 11 additions & 0 deletions js/net/src/lite/announce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)] });
Expand Down
5 changes: 2 additions & 3 deletions js/net/src/lite/announce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
25 changes: 25 additions & 0 deletions js/net/src/lite/subscriber.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions js/net/src/lite/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions js/net/src/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
});
35 changes: 27 additions & 8 deletions js/net/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion quest/m2/audit-compile-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path>` 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).
Expand Down
4 changes: 4 additions & 0 deletions rs/hang/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Relative>`: 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.
Expand Down
4 changes: 2 additions & 2 deletions rs/hang/src/catalog/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<moq_net::path::RelativeOwned>,
pub replay: Option<moq_net::path::Relative>,

/// The publisher instance [`replay`](Self::replay) names, when known. Absent accepts the
/// first one a reader resolves; see [`Reference::epoch`](super::Reference::epoch).
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion rs/hang/src/catalog/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<moq_net::path::RelativeOwned>,
pub broadcast: Option<moq_net::path::Relative>,

/// 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.
Expand Down
2 changes: 1 addition & 1 deletion rs/hang/src/catalog/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<moq_net::path::RelativeOwned>,
pub broadcast: Option<moq_net::path::Relative>,

/// 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.
Expand Down
14 changes: 7 additions & 7 deletions rs/hang/src/catalog/consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::task::{Poll, ready};

use moq_net::PathOwned;
use moq_net::Path;
use serde::de::DeserializeOwned;

use super::Catalog;
Expand All @@ -12,7 +12,7 @@ use crate::Result;
pub struct Consumer<E = ()> {
// Decoded in two steps, so a catalog needing a newer reader fails with a typed error.
inner: moq_json::snapshot::Consumer<serde_json::Value>,
base: PathOwned,
base: Path,
_ext: std::marker::PhantomData<E>,
}

Expand Down Expand Up @@ -75,7 +75,7 @@ impl<E: DeserializeOwned + Default> Consumer<E> {
///
/// 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<E>(base: &PathOwned, catalog: &Catalog<E>) -> Result<()> {
fn check_resolvable<E>(base: &Path, catalog: &Catalog<E>) -> Result<()> {
let video = catalog.video.renditions.iter();
let audio = catalog.audio.renditions.iter();
let text = catalog.text.renditions.iter();
Expand Down Expand Up @@ -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(_))
Expand All @@ -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())
Expand Down
4 changes: 2 additions & 2 deletions rs/hang/src/catalog/echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<moq_net::path::RelativeOwned>) -> Self {
pub fn new(path: impl Into<moq_net::path::Relative>) -> Self {
Self { path: path.into() }
}
}
2 changes: 1 addition & 1 deletion rs/hang/src/catalog/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<moq_net::path::RelativeOwned>,
pub broadcast: Option<moq_net::path::Relative>,

/// 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.
Expand Down
4 changes: 2 additions & 2 deletions rs/hang/src/catalog/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -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()
}

Expand Down
11 changes: 8 additions & 3 deletions rs/hang/src/catalog/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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::<()> {
Expand Down Expand Up @@ -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());
Expand Down
Loading
Loading