Skip to main content

moq_net/
path.rs

1use std::borrow::Cow;
2use std::fmt::{self, Display};
3use std::sync::Arc;
4
5use crate::coding::{Decode, DecodeError, Encode, EncodeError};
6
7/// An owned version of [`Path`] with a `'static` lifetime.
8pub type PathOwned = Path<'static>;
9
10/// A trait for types that can be converted to a `Path`.
11///
12/// When providing a String/str, any leading/trailing slashes are trimmed and multiple consecutive slashes are collapsed.
13/// When already a Path, normalization is skipped and the underlying buffer is reused without copying.
14pub trait AsPath {
15	/// Borrow `self` as a [`Path`], normalizing slashes only when needed.
16	fn as_path(&self) -> Path<'_>;
17}
18
19impl<'a> AsPath for &'a str {
20	fn as_path(&self) -> Path<'a> {
21		Path::new(self)
22	}
23}
24
25impl<'a> AsPath for &'a Path<'a> {
26	fn as_path(&self) -> Path<'a> {
27		// We don't normalize again nor do we copy the bytes.
28		self.borrow()
29	}
30}
31
32impl AsPath for Path<'_> {
33	fn as_path(&self) -> Path<'_> {
34		self.borrow()
35	}
36}
37
38impl AsPath for String {
39	fn as_path(&self) -> Path<'_> {
40		Path::new(self)
41	}
42}
43
44impl<'a> AsPath for &'a String {
45	fn as_path(&self) -> Path<'a> {
46		Path::new(self)
47	}
48}
49
50/// A borrowed slice of the path, or a suffix of a shared reference-counted buffer.
51///
52/// The `Shared` variant is what makes owned paths cheap: cloning bumps a refcount and
53/// suffix operations (strip_prefix, next_part) only advance `start`, so one allocation
54/// serves every copy of a path as it fans out to consumers.
55#[derive(Clone)]
56enum Repr<'a> {
57	Borrowed(&'a str),
58	Shared { buf: Arc<str>, start: usize },
59}
60
61/// A broadcast path that provides safe prefix matching operations.
62///
63/// This type wraps a string but provides path-aware operations that respect
64/// delimiter boundaries, preventing issues like "foo" matching "foobar".
65///
66/// Paths are automatically trimmed of leading and trailing slashes on creation,
67/// making all slashes implicit at boundaries.
68/// All paths are RELATIVE; you cannot join with a leading slash to make an absolute path.
69///
70/// Owned paths ([`PathOwned`]) share one reference-counted allocation: cloning, converting
71/// a shared path with [`Path::to_owned`], and suffix operations like [`Path::strip_prefix`]
72/// do not copy the underlying bytes.
73///
74/// # Examples
75/// ```
76/// use moq_net::{Path};
77///
78/// // Creation automatically trims slashes
79/// let path1 = Path::new("/foo/bar/");
80/// let path2 = Path::new("foo/bar");
81/// assert_eq!(path1, path2);
82///
83/// // Methods accept both &str and Path
84/// let base = Path::new("api/v1");
85/// assert!(base.has_prefix("api"));
86/// assert!(base.has_prefix(&Path::new("api/v1")));
87///
88/// let joined = base.join("users");
89/// assert_eq!(joined.as_str(), "api/v1/users");
90/// ```
91#[derive(Clone)]
92pub struct Path<'a>(Repr<'a>);
93
94impl<'a> Path<'a> {
95	/// Maximum number of slash-separated parts in a path.
96	///
97	/// Matches the IETF moq-transport limit of 32 fields in a namespace tuple.
98	/// moq-lite enforces the same bound: encoding or decoding a deeper path fails,
99	/// and publishing one to an origin is rejected.
100	pub const MAX_PARTS: usize = 32;
101
102	/// Create a new Path from a string slice.
103	///
104	/// Leading and trailing slashes are automatically trimmed.
105	/// Multiple consecutive internal slashes are collapsed to single slashes.
106	pub fn new(s: &'a str) -> Self {
107		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
108
109		// Check if we need to normalize (has multiple consecutive slashes)
110		if trimmed.contains("//") {
111			// Only allocate if we actually need to normalize
112			let normalized = trimmed
113				.split('/')
114				.filter(|s| !s.is_empty())
115				.collect::<Vec<_>>()
116				.join("/");
117			Self(Repr::Shared {
118				buf: normalized.into(),
119				start: 0,
120			})
121		} else {
122			// No normalization needed - use borrowed string
123			Self(Repr::Borrowed(trimmed))
124		}
125	}
126
127	pub(crate) fn from_escaped(s: String) -> PathOwned {
128		if s.is_empty() {
129			Path::empty()
130		} else {
131			Path(Repr::Shared {
132				buf: s.into(),
133				start: 0,
134			})
135		}
136	}
137
138	// A copy of this path skipping the first `n` bytes, reusing the shared buffer when possible.
139	fn slice_from(&'a self, n: usize) -> Path<'a> {
140		match &self.0 {
141			Repr::Borrowed(s) => Path(Repr::Borrowed(&s[n..])),
142			Repr::Shared { buf, start } => Path(Repr::Shared {
143				buf: buf.clone(),
144				start: start + n,
145			}),
146		}
147	}
148
149	/// Check if this path has the given prefix, respecting path boundaries.
150	///
151	/// Unlike String::starts_with, this ensures that "foo" does not match "foobar".
152	/// The prefix must either:
153	/// - Be exactly equal to this path
154	/// - Be followed by a '/' delimiter in the original path
155	/// - Be empty (matches everything)
156	///
157	/// # Examples
158	/// ```
159	/// use moq_net::Path;
160	///
161	/// let path = Path::new("foo/bar");
162	/// assert!(path.has_prefix("foo"));
163	/// assert!(path.has_prefix(&Path::new("foo")));
164	/// assert!(path.has_prefix("foo/"));
165	/// assert!(!path.has_prefix("fo"));
166	///
167	/// let path = Path::new("foobar");
168	/// assert!(!path.has_prefix("foo"));
169	/// ```
170	pub fn has_prefix(&self, prefix: impl AsPath) -> bool {
171		let prefix = prefix.as_path();
172
173		if prefix.is_empty() {
174			return true;
175		}
176
177		let s = self.as_str();
178		if !s.starts_with(prefix.as_str()) {
179			return false;
180		}
181
182		// Check if the prefix is the exact match
183		if s.len() == prefix.len() {
184			return true;
185		}
186
187		// Otherwise, ensure the character after the prefix is a delimiter
188		s.as_bytes().get(prefix.len()) == Some(&b'/')
189	}
190
191	/// The remainder after removing `prefix`, or `None` if it isn't a prefix.
192	///
193	/// Only whole segments match: `a/bc` is not prefixed by `a/b`. An empty prefix
194	/// returns the whole path.
195	pub fn strip_prefix(&'a self, prefix: impl AsPath) -> Option<Path<'a>> {
196		let prefix = prefix.as_path();
197
198		if prefix.is_empty() {
199			return Some(self.borrow());
200		}
201
202		let s = self.as_str();
203		if !s.starts_with(prefix.as_str()) {
204			return None;
205		}
206
207		// Check if the prefix is the exact match
208		if s.len() == prefix.len() {
209			return Some(Path::empty());
210		}
211
212		// Otherwise, ensure the character after the prefix is a delimiter
213		if s.as_bytes().get(prefix.len()) != Some(&b'/') {
214			return None;
215		}
216
217		Some(self.slice_from(prefix.len() + 1))
218	}
219
220	/// Iterate over the slash-separated parts of the path.
221	///
222	/// The empty path has no parts.
223	///
224	/// # Examples
225	/// ```
226	/// use moq_net::Path;
227	///
228	/// let path = Path::new("foo/bar/baz");
229	/// assert_eq!(path.parts().collect::<Vec<_>>(), ["foo", "bar", "baz"]);
230	/// assert_eq!(Path::empty().parts().count(), 0);
231	/// ```
232	pub fn parts(&self) -> impl Iterator<Item = &str> {
233		// Paths are normalized on creation so there are no empty parts to filter,
234		// except that splitting the empty path yields one empty item.
235		self.as_str().split('/').filter(|part| !part.is_empty())
236	}
237
238	/// Strip the directory component of the path, if any, and return the rest of the path.
239	pub fn next_part(&'a self) -> Option<(&'a str, Path<'a>)> {
240		let s = self.as_str();
241		if s.is_empty() {
242			return None;
243		}
244
245		if let Some(i) = s.find('/') {
246			Some((&s[..i], self.slice_from(i + 1)))
247		} else {
248			Some((s, Path::empty()))
249		}
250	}
251
252	/// The normalized path as a string, with no leading or trailing slash.
253	pub fn as_str(&self) -> &str {
254		match &self.0 {
255			Repr::Borrowed(s) => s,
256			Repr::Shared { buf, start } => &buf[*start..],
257		}
258	}
259
260	/// The empty path, which prefixes every other path.
261	pub fn empty() -> Path<'static> {
262		Path(Repr::Borrowed(""))
263	}
264
265	/// Returns `true` if this is the empty path.
266	pub fn is_empty(&self) -> bool {
267		self.as_str().is_empty()
268	}
269
270	/// The length in bytes, not segments.
271	pub fn len(&self) -> usize {
272		self.as_str().len()
273	}
274
275	/// Clone into a `'static` path, sharing the existing buffer when there is one.
276	pub fn to_owned(&self) -> PathOwned {
277		match &self.0 {
278			Repr::Borrowed("") => Path::empty(),
279			Repr::Borrowed(s) => Path(Repr::Shared {
280				buf: Arc::from(*s),
281				start: 0,
282			}),
283			Repr::Shared { buf, start } => Path(Repr::Shared {
284				buf: buf.clone(),
285				start: *start,
286			}),
287		}
288	}
289
290	/// Consume into a `'static` path, reusing the existing buffer when there is one.
291	pub fn into_owned(self) -> PathOwned {
292		match self.0 {
293			Repr::Borrowed("") => Path::empty(),
294			Repr::Borrowed(s) => Path(Repr::Shared {
295				buf: Arc::from(s),
296				start: 0,
297			}),
298			Repr::Shared { buf, start } => Path(Repr::Shared { buf, start }),
299		}
300	}
301
302	/// A copy of this path bound to `self`'s lifetime, without copying the underlying bytes.
303	pub fn borrow(&'a self) -> Path<'a> {
304		self.slice_from(0)
305	}
306
307	/// Join this path with another path component.
308	///
309	/// # Examples
310	/// ```
311	/// use moq_net::Path;
312	///
313	/// let base = Path::new("foo");
314	/// let joined = base.join("bar");
315	/// assert_eq!(joined.as_str(), "foo/bar");
316	///
317	/// let joined = base.join(&Path::new("bar"));
318	/// assert_eq!(joined.as_str(), "foo/bar");
319	/// ```
320	pub fn join(&self, other: impl AsPath) -> PathOwned {
321		let other = other.as_path();
322
323		if self.is_empty() {
324			other.to_owned()
325		} else if other.is_empty() {
326			self.to_owned()
327		} else {
328			// Since paths are trimmed, we always need to add a slash
329			Path(Repr::Shared {
330				buf: format!("{}/{}", self.as_str(), other.as_str()).into(),
331				start: 0,
332			})
333		}
334	}
335
336	/// Resolve a [`PathRelative`] against this path.
337	///
338	/// A non-empty reference replaces the last segment of the base, matching relative URL
339	/// resolution. `..` segments then pop another segment; other segments are appended.
340	/// Excess `..` is a no-op once the base is empty (subsequent named segments still append).
341	/// An empty `rel` returns this path as an owned copy.
342	///
343	/// [`PathRelative::new`] strips empty and redundant `.` segments, but preserves a lone `.`
344	/// so it can reference the base's parent.
345	///
346	/// # Examples
347	/// ```
348	/// use moq_net::{Path, PathRelative};
349	///
350	/// let base = Path::new("a/b/c");
351	/// assert_eq!(base.resolve(&PathRelative::new("./d")).as_str(), "a/b/d");
352	/// assert_eq!(base.resolve(&PathRelative::new(".")).as_str(), "a/b");
353	/// assert_eq!(base.resolve(&PathRelative::new("../d")).as_str(), "a/d");
354	/// ```
355	pub fn resolve(&self, rel: &PathRelative<'_>) -> PathOwned {
356		if rel.is_empty() {
357			return self.to_owned();
358		}
359
360		let mut segments: Vec<&str> = self.parts().collect();
361		segments.pop();
362
363		for seg in rel.as_str().split('/') {
364			if seg == "." {
365				continue;
366			} else if seg == ".." {
367				segments.pop();
368			} else {
369				segments.push(seg);
370			}
371		}
372
373		let path = segments.join("/");
374		if path.is_empty() {
375			Path::empty()
376		} else {
377			Path(Repr::Shared {
378				buf: path.into(),
379				start: 0,
380			})
381		}
382	}
383
384	/// Resolve a [`PathRelative`], returning `None` if it escapes above the root.
385	///
386	/// Unlike [`Path::resolve`], this distinguishes a valid reference to the empty root
387	/// path from excess `..` segments. Use it when an untrusted relative reference must
388	/// not be clamped to the root.
389	pub fn try_resolve(&self, rel: &PathRelative<'_>) -> Option<PathOwned> {
390		if rel.is_empty() {
391			return Some(self.to_owned());
392		}
393
394		let mut segments: Vec<&str> = self.parts().collect();
395		segments.pop();
396
397		for seg in rel.as_str().split('/') {
398			if seg == "." {
399				continue;
400			} else if seg == ".." {
401				segments.pop()?;
402			} else {
403				segments.push(seg);
404			}
405		}
406
407		let path = segments.join("/");
408		if path.is_empty() {
409			Some(Path::empty())
410		} else {
411			Some(Path(Repr::Shared {
412				buf: path.into(),
413				start: 0,
414			}))
415		}
416	}
417
418	/// Express this path relative to `base`: the inverse of [`Path::resolve`].
419	///
420	/// The result round-trips (`base.resolve(&rel) == self`) and never walks above the
421	/// root, so [`Path::try_resolve`] accepts it too.
422	///
423	/// A relative reference replaces the last segment of the base, matching relative URL
424	/// resolution, so a target nested under the base repeats the base's own last segment.
425	///
426	/// The empty reference names the base itself, so that is what a self-reference returns.
427	///
428	/// Returns `None` for a target no reference can name: a path segment may literally be
429	/// `.` or `..`, which resolution reads as navigation instead of as a name. Only the
430	/// segments past the shared prefix matter, since the rest are never emitted.
431	///
432	/// # Examples
433	/// ```
434	/// use moq_net::Path;
435	///
436	/// // The base names a broadcast, so its last segment is replaced, not descended into.
437	/// let base = Path::new("a/b");
438	/// assert_eq!(Path::new("a/b/c").relative(&base).unwrap().as_str(), "b/c");
439	/// assert_eq!(Path::new("a/c").relative(&base).unwrap().as_str(), "c");
440	/// assert_eq!(Path::new("c").relative(&base).unwrap().as_str(), "../c");
441	///
442	/// // The lone `.` names the base's parent, which the empty reference cannot.
443	/// assert_eq!(Path::new("a").relative(&base).unwrap().as_str(), ".");
444	///
445	/// // The base itself.
446	/// assert_eq!(Path::new("a/b").relative(&base).unwrap().as_str(), "");
447	///
448	/// // A segment named `..` is a legal path component but an unnameable target.
449	/// assert!(Path::new("a/..").relative(&base).is_none());
450	/// ```
451	pub fn relative(&self, base: impl AsPath) -> Option<PathRelativeOwned> {
452		let base = base.as_path();
453
454		// Only the empty reference can name a base whose last segment is itself `.` or `..`,
455		// since resolution replaces that segment rather than emitting it.
456		if *self == base {
457			return Some(PathRelative::empty());
458		}
459
460		// Resolution replaces the base's last segment, so walk from its parent.
461		let mut dir: Vec<&str> = base.parts().collect();
462		dir.pop();
463
464		let target: Vec<&str> = self.parts().collect();
465		let common = dir.iter().zip(&target).take_while(|(a, b)| a == b).count();
466
467		let down = &target[common..];
468		if down.iter().any(|part| *part == "." || *part == "..") {
469			// Resolution would walk on these instead of naming them.
470			return None;
471		}
472
473		let mut rel: Vec<&str> = vec![".."; dir.len() - common];
474		rel.extend(down);
475
476		if rel.is_empty() {
477			// An empty reference resolves to the base itself, so name the parent explicitly.
478			return Some(PathRelative::new("."));
479		}
480
481		Some(PathRelativeOwned::from(rel.join("/")))
482	}
483}
484
485// Comparisons, ordering, and hashing all go through `as_str()` so a borrowed and a
486// shared path with the same content behave identically (e.g. as map keys).
487impl<'b> PartialEq<Path<'b>> for Path<'_> {
488	fn eq(&self, other: &Path<'b>) -> bool {
489		self.as_str() == other.as_str()
490	}
491}
492
493impl Eq for Path<'_> {}
494
495impl PartialOrd for Path<'_> {
496	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
497		Some(self.cmp(other))
498	}
499}
500
501impl Ord for Path<'_> {
502	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
503		self.as_str().cmp(other.as_str())
504	}
505}
506
507impl std::hash::Hash for Path<'_> {
508	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
509		self.as_str().hash(state)
510	}
511}
512
513impl fmt::Debug for Path<'_> {
514	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515		f.debug_tuple("Path").field(&self.as_str()).finish()
516	}
517}
518
519impl serde::Serialize for Path<'_> {
520	fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
521		serializer.serialize_str(self.as_str())
522	}
523}
524
525impl<'a> From<&'a str> for Path<'a> {
526	fn from(s: &'a str) -> Self {
527		Self::new(s)
528	}
529}
530
531impl<'a> From<&'a String> for Path<'a> {
532	fn from(s: &'a String) -> Self {
533		// TODO avoid making a copy here
534		Self::new(s)
535	}
536}
537
538impl Default for Path<'_> {
539	fn default() -> Self {
540		Path::empty()
541	}
542}
543
544impl From<String> for Path<'_> {
545	fn from(s: String) -> Self {
546		Path::new(&s).into_owned()
547	}
548}
549
550impl AsRef<str> for Path<'_> {
551	fn as_ref(&self) -> &str {
552		self.as_str()
553	}
554}
555
556impl Display for Path<'_> {
557	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558		write!(f, "{}", self.as_str())
559	}
560}
561
562impl<V: Copy> Decode<V> for Path<'_>
563where
564	String: Decode<V>,
565{
566	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
567		let path: Path = String::decode(r, version)?.into();
568		if path.parts().count() > Path::MAX_PARTS {
569			return Err(DecodeError::BoundsExceeded);
570		}
571		Ok(path)
572	}
573}
574
575impl<V: Copy> Encode<V> for Path<'_>
576where
577	for<'a> &'a str: Encode<V>,
578{
579	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
580		if self.parts().count() > Path::MAX_PARTS {
581			return Err(EncodeError::BoundsExceeded);
582		}
583		self.as_str().encode(w, version)?;
584		Ok(())
585	}
586}
587
588/// An owned version of [`PathRelative`] with a `'static` lifetime.
589pub type PathRelativeOwned = PathRelative<'static>;
590
591/// A relative broadcast path, used to reference one broadcast from another broadcast's content.
592///
593/// Unlike [`Path`] (which is a complete reference within the broadcast namespace),
594/// `PathRelative` may contain `.` and `..` segments to walk the namespace and is meaningful
595/// only when resolved against a base [`Path`] via [`Path::resolve`]. The hang catalog uses it
596/// to point a rendition at a track published in a sibling broadcast (e.g. `./source`).
597///
598/// `PathRelative` has no `Encode`/`Decode` impl, so it never appears in announce/subscribe
599/// frames. It does serialize via serde for off-wire use (e.g. as a field inside a catalog
600/// JSON payload, which itself travels as a track).
601///
602/// Normalization on creation: leading/trailing slashes are trimmed, consecutive internal
603/// slashes collapse to one, and redundant `.` segments are stripped. A reference made only
604/// of `.` segments normalizes to `.` rather than empty because `.` resolves to the base's
605/// parent while empty resolves to the base itself. `..` is preserved for resolve time.
606///
607/// # Examples
608/// ```
609/// use moq_net::{Path, PathRelative};
610///
611/// let rel = PathRelative::new("./source");
612/// assert_eq!(Path::new("a/b").resolve(&rel).as_str(), "a/source");
613///
614/// // Redundant `.` segments are stripped on creation.
615/// assert_eq!(PathRelative::new("./a/./b").as_str(), "a/b");
616/// assert_eq!(PathRelative::new(".").as_str(), ".");
617/// ```
618#[derive(Debug, PartialEq, Eq, Hash, Clone, serde::Serialize)]
619pub struct PathRelative<'a>(Cow<'a, str>);
620
621impl<'a> PathRelative<'a> {
622	/// Create a new `PathRelative` from a string slice.
623	///
624	/// Leading and trailing slashes are trimmed, consecutive internal slashes collapse to one,
625	/// and redundant `.` segments are stripped. See the type-level doc for the full rules.
626	pub fn new(s: &'a str) -> Self {
627		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
628
629		if needs_normalize_relative(trimmed) {
630			Self(Cow::Owned(normalize_relative_segments(trimmed)))
631		} else {
632			Self(Cow::Borrowed(trimmed))
633		}
634	}
635
636	/// The normalized path as a string slice.
637	pub fn as_str(&self) -> &str {
638		&self.0
639	}
640
641	/// The empty relative path, which resolves to the base path itself.
642	pub fn empty() -> PathRelative<'static> {
643		PathRelative(Cow::Borrowed(""))
644	}
645
646	/// True if the path is empty (resolves to the base path itself).
647	pub fn is_empty(&self) -> bool {
648		self.0.is_empty()
649	}
650
651	/// The length of the normalized path in bytes.
652	pub fn len(&self) -> usize {
653		self.0.len()
654	}
655
656	/// Copy into an owned version with a `'static` lifetime.
657	pub fn to_owned(&self) -> PathRelativeOwned {
658		PathRelative(Cow::Owned(self.0.to_string()))
659	}
660
661	/// Convert into an owned version with a `'static` lifetime.
662	pub fn into_owned(self) -> PathRelativeOwned {
663		PathRelative(Cow::Owned(self.0.into_owned()))
664	}
665
666	/// Reborrow without copying.
667	pub fn borrow(&'a self) -> PathRelative<'a> {
668		PathRelative(Cow::Borrowed(&self.0))
669	}
670}
671
672impl<'a> From<&'a str> for PathRelative<'a> {
673	fn from(s: &'a str) -> Self {
674		Self::new(s)
675	}
676}
677
678impl<'a> From<&'a String> for PathRelative<'a> {
679	fn from(s: &'a String) -> Self {
680		Self::new(s)
681	}
682}
683
684impl From<String> for PathRelative<'_> {
685	fn from(s: String) -> Self {
686		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
687
688		if needs_normalize_relative(trimmed) {
689			Self(Cow::Owned(normalize_relative_segments(trimmed)))
690		} else if trimmed == s {
691			Self(Cow::Owned(s))
692		} else {
693			Self(Cow::Owned(trimmed.to_string()))
694		}
695	}
696}
697
698fn needs_normalize_relative(trimmed: &str) -> bool {
699	trimmed.split('/').any(|seg| seg.is_empty() || seg == ".")
700}
701
702fn normalize_relative_segments(trimmed: &str) -> String {
703	let segments = trimmed
704		.split('/')
705		.filter(|seg| !seg.is_empty() && *seg != ".")
706		.collect::<Vec<_>>()
707		.join("/");
708
709	if segments.is_empty() && trimmed.split('/').any(|seg| seg == ".") {
710		".".to_string()
711	} else {
712		segments
713	}
714}
715
716impl Default for PathRelative<'_> {
717	fn default() -> Self {
718		Self(Cow::Borrowed(""))
719	}
720}
721
722impl AsRef<str> for PathRelative<'_> {
723	fn as_ref(&self) -> &str {
724		&self.0
725	}
726}
727
728impl Display for PathRelative<'_> {
729	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730		write!(f, "{}", self.0)
731	}
732}
733
734// Owned-only deserialization. We use `String::deserialize` so that owned deserializers
735// (e.g. `serde_json::from_slice`) work. The borrowed form `<&str>::deserialize` requires
736// `'de: 'a`, which is unsatisfiable when `'a = 'static`.
737impl<'de> serde::Deserialize<'de> for PathRelative<'static> {
738	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
739	where
740		D: serde::Deserializer<'de>,
741	{
742		let s = String::deserialize(deserializer)?;
743		Ok(PathRelative::from(s))
744	}
745}
746
747/// A deduplicated list of path prefixes.
748///
749/// Automatically removes exact duplicates and overlapping prefixes on construction.
750/// For example, `["demo", "demo/foo", "anon"]` becomes `["demo", "anon"]` since
751/// `"demo"` already covers `"demo/foo"`.
752#[derive(Debug, Clone, Default, Eq)]
753pub struct PathPrefixes {
754	paths: Vec<PathOwned>,
755}
756
757impl PathPrefixes {
758	/// Create a new PathPrefixes, deduplicating and removing overlapping prefixes.
759	///
760	/// Shorter prefixes subsume longer ones: `"demo"` covers `"demo/foo"`.
761	///
762	/// Accepts anything iterable over path-like items:
763	/// ```
764	/// use moq_net::PathPrefixes;
765	///
766	/// let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
767	/// assert_eq!(list.len(), 2); // "demo/foo" subsumed by "demo"
768	/// ```
769	pub fn new(paths: impl IntoIterator<Item = impl AsPath>) -> Self {
770		let mut paths: Vec<PathOwned> = paths.into_iter().map(|p| p.as_path().to_owned()).collect();
771
772		if paths.len() <= 1 {
773			return Self { paths };
774		}
775
776		// Sort by length so shorter (more permissive) prefixes come first.
777		// Tie-break lexicographically for canonical ordering.
778		paths.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.as_str().cmp(b.as_str())));
779		paths.dedup();
780
781		let mut result: Vec<PathOwned> = Vec::new();
782		'outer: for path in paths {
783			for existing in &result {
784				if path.has_prefix(existing) {
785					continue 'outer;
786				}
787			}
788			result.push(path);
789		}
790
791		Self { paths: result }
792	}
793
794	/// Returns `true` if the set contains no prefixes, so it matches nothing.
795	pub fn is_empty(&self) -> bool {
796		self.paths.is_empty()
797	}
798
799	/// The number of prefixes, after redundant ones were collapsed.
800	pub fn len(&self) -> usize {
801		self.paths.len()
802	}
803
804	/// Iterate the prefixes in the set.
805	pub fn iter(&self) -> std::slice::Iter<'_, PathOwned> {
806		self.paths.iter()
807	}
808}
809
810impl std::ops::Deref for PathPrefixes {
811	type Target = [PathOwned];
812
813	fn deref(&self) -> &[PathOwned] {
814		&self.paths
815	}
816}
817
818impl FromIterator<PathOwned> for PathPrefixes {
819	fn from_iter<I: IntoIterator<Item = PathOwned>>(iter: I) -> Self {
820		Self::new(iter)
821	}
822}
823
824impl From<Vec<PathOwned>> for PathPrefixes {
825	fn from(paths: Vec<PathOwned>) -> Self {
826		Self::new(paths)
827	}
828}
829
830impl<'a> PartialEq<Vec<Path<'a>>> for PathPrefixes {
831	fn eq(&self, other: &Vec<Path<'a>>) -> bool {
832		self.paths == *other
833	}
834}
835
836impl<'a> PartialEq<PathPrefixes> for Vec<Path<'a>> {
837	fn eq(&self, other: &PathPrefixes) -> bool {
838		*self == other.paths
839	}
840}
841
842impl PartialEq for PathPrefixes {
843	fn eq(&self, other: &Self) -> bool {
844		self.paths == other.paths
845	}
846}
847
848impl IntoIterator for PathPrefixes {
849	type Item = PathOwned;
850	type IntoIter = std::vec::IntoIter<PathOwned>;
851
852	fn into_iter(self) -> Self::IntoIter {
853		self.paths.into_iter()
854	}
855}
856
857impl<'a> IntoIterator for &'a PathPrefixes {
858	type Item = &'a PathOwned;
859	type IntoIter = std::slice::Iter<'a, PathOwned>;
860
861	fn into_iter(self) -> Self::IntoIter {
862		self.paths.iter()
863	}
864}
865
866#[cfg(test)]
867mod tests {
868	use super::*;
869
870	#[test]
871	fn test_has_prefix() {
872		let path = Path::new("foo/bar/baz");
873
874		// Valid prefixes - test with both &str and &Path
875		assert!(path.has_prefix(""));
876		assert!(path.has_prefix("foo"));
877		assert!(path.has_prefix(Path::new("foo")));
878		assert!(path.has_prefix("foo/"));
879		assert!(path.has_prefix("foo/bar"));
880		assert!(path.has_prefix(Path::new("foo/bar/")));
881		assert!(path.has_prefix("foo/bar/baz"));
882
883		// Invalid prefixes - should not match partial components
884		assert!(!path.has_prefix("f"));
885		assert!(!path.has_prefix(Path::new("fo")));
886		assert!(!path.has_prefix("foo/b"));
887		assert!(!path.has_prefix("foo/ba"));
888		assert!(!path.has_prefix(Path::new("foo/bar/ba")));
889
890		// Edge case: "foobar" should not match "foo"
891		let path = Path::new("foobar");
892		assert!(!path.has_prefix("foo"));
893		assert!(path.has_prefix(Path::new("foobar")));
894	}
895
896	#[test]
897	fn test_strip_prefix() {
898		let path = Path::new("foo/bar/baz");
899
900		// Test with both &str and &Path
901		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
902		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
903		assert_eq!(path.strip_prefix(Path::new("foo/")).unwrap().as_str(), "bar/baz");
904		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
905		assert_eq!(path.strip_prefix(Path::new("foo/bar/")).unwrap().as_str(), "baz");
906		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
907
908		// Should fail for invalid prefixes
909		assert!(path.strip_prefix("fo").is_none());
910		assert!(path.strip_prefix(Path::new("bar")).is_none());
911	}
912
913	#[test]
914	fn test_join() {
915		// Test with both &str and &Path
916		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
917		assert_eq!(Path::new("foo/").join(Path::new("bar")).as_str(), "foo/bar");
918		assert_eq!(Path::new("").join("bar").as_str(), "bar");
919		assert_eq!(Path::new("foo/bar").join(Path::new("baz")).as_str(), "foo/bar/baz");
920	}
921
922	#[test]
923	fn test_empty() {
924		let empty = Path::new("");
925		assert!(empty.is_empty());
926		assert_eq!(empty.len(), 0);
927
928		let non_empty = Path::new("foo");
929		assert!(!non_empty.is_empty());
930		assert_eq!(non_empty.len(), 3);
931	}
932
933	#[test]
934	fn test_from_conversions() {
935		let path1 = Path::from("foo/bar");
936		let path2 = Path::from("foo/bar");
937		let s = String::from("foo/bar");
938		let path3 = Path::from(&s);
939
940		assert_eq!(path1.as_str(), "foo/bar");
941		assert_eq!(path2.as_str(), "foo/bar");
942		assert_eq!(path3.as_str(), "foo/bar");
943	}
944
945	#[test]
946	fn test_path_prefix_join() {
947		let prefix = Path::new("foo");
948		let suffix = Path::new("bar/baz");
949		let path = prefix.join(&suffix);
950		assert_eq!(path.as_str(), "foo/bar/baz");
951
952		let prefix = Path::new("foo/");
953		let suffix = Path::new("bar/baz");
954		let path = prefix.join(&suffix);
955		assert_eq!(path.as_str(), "foo/bar/baz");
956
957		let prefix = Path::new("foo");
958		let suffix = Path::new("/bar/baz");
959		let path = prefix.join(&suffix);
960		assert_eq!(path.as_str(), "foo/bar/baz");
961
962		let prefix = Path::new("");
963		let suffix = Path::new("bar/baz");
964		let path = prefix.join(&suffix);
965		assert_eq!(path.as_str(), "bar/baz");
966	}
967
968	#[test]
969	fn test_path_prefix_conversions() {
970		let prefix1 = Path::from("foo/bar");
971		let prefix2 = Path::from(String::from("foo/bar"));
972		let s = String::from("foo/bar");
973		let prefix3 = Path::from(&s);
974
975		assert_eq!(prefix1.as_str(), "foo/bar");
976		assert_eq!(prefix2.as_str(), "foo/bar");
977		assert_eq!(prefix3.as_str(), "foo/bar");
978	}
979
980	#[test]
981	fn test_path_suffix_conversions() {
982		let suffix1 = Path::from("foo/bar");
983		let suffix2 = Path::from(String::from("foo/bar"));
984		let s = String::from("foo/bar");
985		let suffix3 = Path::from(&s);
986
987		assert_eq!(suffix1.as_str(), "foo/bar");
988		assert_eq!(suffix2.as_str(), "foo/bar");
989		assert_eq!(suffix3.as_str(), "foo/bar");
990	}
991
992	#[test]
993	fn test_path_types_basic_operations() {
994		let prefix = Path::new("foo/bar");
995		assert_eq!(prefix.as_str(), "foo/bar");
996		assert!(!prefix.is_empty());
997		assert_eq!(prefix.len(), 7);
998
999		let suffix = Path::new("baz/qux");
1000		assert_eq!(suffix.as_str(), "baz/qux");
1001		assert!(!suffix.is_empty());
1002		assert_eq!(suffix.len(), 7);
1003
1004		let empty_prefix = Path::new("");
1005		assert!(empty_prefix.is_empty());
1006		assert_eq!(empty_prefix.len(), 0);
1007
1008		let empty_suffix = Path::new("");
1009		assert!(empty_suffix.is_empty());
1010		assert_eq!(empty_suffix.len(), 0);
1011	}
1012
1013	#[test]
1014	fn test_prefix_has_prefix() {
1015		// Test empty prefix (should match everything)
1016		let prefix = Path::new("foo/bar");
1017		assert!(prefix.has_prefix(""));
1018
1019		// Test exact matches
1020		let prefix = Path::new("foo/bar");
1021		assert!(prefix.has_prefix("foo/bar"));
1022
1023		// Test valid prefixes
1024		assert!(prefix.has_prefix("foo"));
1025		assert!(prefix.has_prefix("foo/"));
1026
1027		// Test invalid prefixes - partial matches should fail
1028		assert!(!prefix.has_prefix("f"));
1029		assert!(!prefix.has_prefix("fo"));
1030		assert!(!prefix.has_prefix("foo/b"));
1031		assert!(!prefix.has_prefix("foo/ba"));
1032
1033		// Test edge cases
1034		let prefix = Path::new("foobar");
1035		assert!(!prefix.has_prefix("foo"));
1036		assert!(prefix.has_prefix("foobar"));
1037
1038		// Test trailing slash handling
1039		let prefix = Path::new("foo/bar/");
1040		assert!(prefix.has_prefix("foo"));
1041		assert!(prefix.has_prefix("foo/"));
1042		assert!(prefix.has_prefix("foo/bar"));
1043		assert!(prefix.has_prefix("foo/bar/"));
1044
1045		// Test single component
1046		let prefix = Path::new("foo");
1047		assert!(prefix.has_prefix(""));
1048		assert!(prefix.has_prefix("foo"));
1049		assert!(prefix.has_prefix("foo/")); // "foo/" becomes "foo" after trimming
1050		assert!(!prefix.has_prefix("f"));
1051
1052		// Test empty prefix
1053		let prefix = Path::new("");
1054		assert!(prefix.has_prefix(""));
1055		assert!(!prefix.has_prefix("foo"));
1056	}
1057
1058	#[test]
1059	fn test_prefix_join() {
1060		// Basic joining
1061		let prefix = Path::new("foo");
1062		let suffix = Path::new("bar");
1063		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
1064
1065		// Trailing slash on prefix
1066		let prefix = Path::new("foo/");
1067		let suffix = Path::new("bar");
1068		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
1069
1070		// Leading slash on suffix
1071		let prefix = Path::new("foo");
1072		let suffix = Path::new("/bar");
1073		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
1074
1075		// Trailing slash on suffix
1076		let prefix = Path::new("foo");
1077		let suffix = Path::new("bar/");
1078		assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); // trailing slash is trimmed
1079
1080		// Both have slashes
1081		let prefix = Path::new("foo/");
1082		let suffix = Path::new("/bar");
1083		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
1084
1085		// Empty suffix
1086		let prefix = Path::new("foo");
1087		let suffix = Path::new("");
1088		assert_eq!(prefix.join(suffix).as_str(), "foo");
1089
1090		// Empty prefix
1091		let prefix = Path::new("");
1092		let suffix = Path::new("bar");
1093		assert_eq!(prefix.join(suffix).as_str(), "bar");
1094
1095		// Both empty
1096		let prefix = Path::new("");
1097		let suffix = Path::new("");
1098		assert_eq!(prefix.join(suffix).as_str(), "");
1099
1100		// Complex paths
1101		let prefix = Path::new("foo/bar");
1102		let suffix = Path::new("baz/qux");
1103		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux");
1104
1105		// Complex paths with slashes
1106		let prefix = Path::new("foo/bar/");
1107		let suffix = Path::new("/baz/qux/");
1108		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
1109	}
1110
1111	#[test]
1112	fn test_path_ref() {
1113		// Test PathRef creation and normalization
1114		let ref1 = Path::new("/foo/bar/");
1115		assert_eq!(ref1.as_str(), "foo/bar");
1116
1117		let ref2 = Path::from("///foo///");
1118		assert_eq!(ref2.as_str(), "foo");
1119
1120		// Test PathRef normalizes multiple slashes
1121		let ref3 = Path::new("foo//bar///baz");
1122		assert_eq!(ref3.as_str(), "foo/bar/baz");
1123
1124		// Test conversions
1125		let path = Path::new("foo/bar");
1126		let path_ref = path;
1127		assert_eq!(path_ref.as_str(), "foo/bar");
1128
1129		// Test that Path methods work with PathRef
1130		let path2 = Path::new("foo/bar/baz");
1131		assert!(path2.has_prefix(&path_ref));
1132		assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz");
1133
1134		// Test empty PathRef
1135		let empty = Path::new("");
1136		assert!(empty.is_empty());
1137		assert_eq!(empty.len(), 0);
1138	}
1139
1140	#[test]
1141	fn test_multiple_consecutive_slashes() {
1142		let path = Path::new("foo//bar///baz");
1143		// Multiple consecutive slashes are collapsed to single slashes
1144		assert_eq!(path.as_str(), "foo/bar/baz");
1145
1146		// Test with leading and trailing slashes too
1147		let path2 = Path::new("//foo//bar///baz//");
1148		assert_eq!(path2.as_str(), "foo/bar/baz");
1149
1150		// Test empty segments are handled correctly
1151		let path3 = Path::new("foo///bar");
1152		assert_eq!(path3.as_str(), "foo/bar");
1153	}
1154
1155	#[test]
1156	fn test_removes_multiple_slashes_comprehensively() {
1157		// Test various multiple slash scenarios
1158		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
1159		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
1160		assert_eq!(Path::new("foo////bar").as_str(), "foo/bar");
1161
1162		// Multiple occurrences of double slashes
1163		assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz");
1164		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
1165
1166		// Mixed slash counts
1167		assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux");
1168
1169		// With leading and trailing slashes
1170		assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar");
1171		assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar");
1172
1173		// Edge case: only slashes
1174		assert_eq!(Path::new("//").as_str(), "");
1175		assert_eq!(Path::new("////").as_str(), "");
1176
1177		// Test that operations work correctly with normalized paths
1178		let path_with_slashes = Path::new("foo//bar///baz");
1179		assert!(path_with_slashes.has_prefix("foo/bar"));
1180		assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz");
1181		assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux");
1182
1183		// Test PathRef to Path conversion
1184		let path_ref = Path::new("foo//bar///baz");
1185		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too
1186		let path_from_ref = path_ref.to_owned();
1187		assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized
1188	}
1189
1190	#[test]
1191	fn test_path_ref_multiple_slashes() {
1192		// PathRef now normalizes multiple slashes using Cow
1193		let path_ref = Path::new("//foo//bar///baz//");
1194		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized
1195
1196		// Various multiple slash scenarios are normalized in PathRef
1197		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
1198		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
1199		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
1200
1201		// Conversion to Path maintains normalized form
1202		assert_eq!(Path::new("foo//bar").to_owned().as_str(), "foo/bar");
1203		assert_eq!(Path::new("foo///bar").to_owned().as_str(), "foo/bar");
1204		assert_eq!(Path::new("a//b//c//d").to_owned().as_str(), "a/b/c/d");
1205
1206		// Edge cases
1207		assert_eq!(Path::new("//").as_str(), "");
1208		assert_eq!(Path::new("////").as_str(), "");
1209		assert_eq!(Path::new("//").to_owned().as_str(), "");
1210		assert_eq!(Path::new("////").to_owned().as_str(), "");
1211
1212		// Test that PathRef avoids allocation when no normalization needed
1213		let normal_path = Path::new("foo/bar/baz");
1214		assert_eq!(normal_path.as_str(), "foo/bar/baz");
1215		// This should use Cow::Borrowed internally (no allocation)
1216
1217		let needs_norm = Path::new("foo//bar");
1218		assert_eq!(needs_norm.as_str(), "foo/bar");
1219		// This should use Cow::Owned internally (allocation only when needed)
1220	}
1221
1222	#[test]
1223	fn test_ergonomic_conversions() {
1224		// Test that all these work ergonomically in function calls
1225		fn takes_path_ref<'a>(p: impl Into<Path<'a>>) -> String {
1226			p.into().as_str().to_string()
1227		}
1228
1229		// Alternative API using the trait alias for better error messages
1230		fn takes_path_ref_with_trait<'a>(p: impl Into<Path<'a>>) -> String {
1231			p.into().as_str().to_string()
1232		}
1233
1234		// String literal
1235		assert_eq!(takes_path_ref("foo//bar"), "foo/bar");
1236
1237		// String (owned) - this should now work without &
1238		let owned_string = String::from("foo//bar///baz");
1239		assert_eq!(takes_path_ref(owned_string), "foo/bar/baz");
1240
1241		// &String
1242		let string_ref = String::from("foo//bar");
1243		assert_eq!(takes_path_ref(string_ref), "foo/bar");
1244
1245		// PathRef
1246		let path_ref = Path::new("foo//bar");
1247		assert_eq!(takes_path_ref(path_ref), "foo/bar");
1248
1249		// Path
1250		let path = Path::new("foo//bar");
1251		assert_eq!(takes_path_ref(path), "foo/bar");
1252
1253		// Test that Path::new works with all these types
1254		let _path1 = Path::new("foo/bar"); // &str
1255		let _path2 = Path::new("foo/bar"); // String - should now work
1256		let _path3 = Path::new("foo/bar"); // &String
1257		let _path4 = Path::new("foo/bar"); // PathRef
1258
1259		// Test the trait alias version works the same
1260		assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar");
1261		assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar");
1262	}
1263
1264	#[test]
1265	fn test_prefix_strip_prefix() {
1266		// Test basic stripping
1267		let prefix = Path::new("foo/bar/baz");
1268		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
1269		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar/baz");
1270		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar/baz");
1271		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "baz");
1272		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "baz");
1273		assert_eq!(prefix.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
1274
1275		// Test invalid prefixes
1276		assert!(prefix.strip_prefix("fo").is_none());
1277		assert!(prefix.strip_prefix("bar").is_none());
1278		assert!(prefix.strip_prefix("foo/ba").is_none());
1279
1280		// Test edge cases
1281		let prefix = Path::new("foobar");
1282		assert!(prefix.strip_prefix("foo").is_none());
1283		assert_eq!(prefix.strip_prefix("foobar").unwrap().as_str(), "");
1284
1285		// Test empty prefix
1286		let prefix = Path::new("");
1287		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "");
1288		assert!(prefix.strip_prefix("foo").is_none());
1289
1290		// Test single component
1291		let prefix = Path::new("foo");
1292		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "");
1293		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
1294
1295		// Test trailing slash handling
1296		let prefix = Path::new("foo/bar/");
1297		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar");
1298		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar");
1299		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "");
1300		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "");
1301	}
1302
1303	#[test]
1304	fn test_prefix_list_dedup() {
1305		// Exact duplicates are removed
1306		let list = PathPrefixes::new(["demo", "demo"]);
1307		assert_eq!(list.len(), 1);
1308		assert_eq!(list[0], Path::new("demo"));
1309	}
1310
1311	#[test]
1312	fn test_prefix_list_overlap() {
1313		// "demo/foo" is redundant when "demo" exists
1314		let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
1315		assert_eq!(list.len(), 2);
1316		assert!(list.iter().any(|p| p == &Path::new("demo")));
1317		assert!(list.iter().any(|p| p == &Path::new("anon")));
1318	}
1319
1320	#[test]
1321	fn test_prefix_list_overlap_reverse_order() {
1322		// Order shouldn't matter
1323		let list = PathPrefixes::new(["demo/foo", "demo"]);
1324		assert_eq!(list.len(), 1);
1325		assert_eq!(list[0], Path::new("demo"));
1326	}
1327
1328	#[test]
1329	fn test_prefix_list_empty_covers_all() {
1330		// Empty prefix covers everything
1331		let list = PathPrefixes::new(["", "demo", "anon"]);
1332		assert_eq!(list.len(), 1);
1333		assert_eq!(list[0], Path::new(""));
1334	}
1335
1336	#[test]
1337	fn test_prefix_list_no_overlap() {
1338		// Unrelated prefixes are all kept
1339		let list = PathPrefixes::new(["demo", "anon", "secret"]);
1340		assert_eq!(list.len(), 3);
1341	}
1342
1343	#[test]
1344	fn test_prefix_list_single() {
1345		let list = PathPrefixes::new(["demo"]);
1346		assert_eq!(list.len(), 1);
1347	}
1348
1349	#[test]
1350	fn test_prefix_list_empty() {
1351		let list = PathPrefixes::new(std::iter::empty::<&str>());
1352		assert!(list.is_empty());
1353		assert_eq!(list.len(), 0);
1354	}
1355
1356	#[test]
1357	fn test_prefix_list_deep_overlap() {
1358		// "a/b/c" is covered by "a/b" which is covered by "a"
1359		let list = PathPrefixes::new(["a/b/c", "a/b", "a"]);
1360		assert_eq!(list.len(), 1);
1361		assert_eq!(list[0], Path::new("a"));
1362	}
1363
1364	#[test]
1365	fn test_prefix_list_partial_name_not_overlap() {
1366		// "demo" should NOT cover "demonstration" (different path component)
1367		let list = PathPrefixes::new(["demo", "demonstration"]);
1368		assert_eq!(list.len(), 2);
1369	}
1370
1371	#[test]
1372	fn test_prefix_list_collect() {
1373		let paths: Vec<PathOwned> = vec!["demo".into(), "demo/foo".into()];
1374		let list: PathPrefixes = paths.into_iter().collect();
1375		assert_eq!(list.len(), 1);
1376		assert_eq!(list[0], Path::new("demo"));
1377	}
1378
1379	#[test]
1380	fn test_prefix_list_eq_vec() {
1381		let list = PathPrefixes::new(["demo", "anon"]);
1382		// Canonical order: sorted by length, then lexicographically
1383		assert_eq!(list, vec!["anon".as_path(), "demo".as_path()]);
1384	}
1385
1386	// Pointer-equality checks that owned paths share one allocation through the
1387	// clone / to_owned / strip_prefix flow used by origin announce fan-out.
1388	#[test]
1389	fn test_owned_paths_share_allocation() {
1390		let path = Path::new("customer/room/broadcast").to_owned();
1391
1392		// Cloning an owned path shares the buffer.
1393		let cloned = path.clone();
1394		assert_eq!(path.as_str().as_ptr(), cloned.as_str().as_ptr());
1395
1396		// as_path + to_owned (how notify queues a path per consumer) shares too.
1397		let requeued = path.as_path().to_owned();
1398		assert_eq!(path.as_str().as_ptr(), requeued.as_str().as_ptr());
1399
1400		// Stripping a prefix from an owned path is offset arithmetic, not a copy.
1401		let stripped = path.strip_prefix("customer").unwrap().to_owned();
1402		assert_eq!(stripped.as_str(), "room/broadcast");
1403		assert_eq!(stripped.as_str().as_ptr(), path.as_str()["customer/".len()..].as_ptr());
1404
1405		// next_part shares the rest as well.
1406		let (dir, rest) = path.next_part().unwrap();
1407		assert_eq!(dir, "customer");
1408		let rest = rest.to_owned();
1409		assert_eq!(rest.as_str().as_ptr(), stripped.as_str().as_ptr());
1410
1411		// join produces an owned path whose clones share.
1412		let joined = path.join("alice");
1413		let joined2 = joined.clone();
1414		assert_eq!(joined.as_str(), "customer/room/broadcast/alice");
1415		assert_eq!(joined.as_str().as_ptr(), joined2.as_str().as_ptr());
1416	}
1417
1418	#[test]
1419	fn test_parts() {
1420		assert_eq!(Path::empty().parts().count(), 0);
1421		assert_eq!(Path::new("foo").parts().collect::<Vec<_>>(), ["foo"]);
1422		assert_eq!(Path::new("/foo//bar/").parts().collect::<Vec<_>>(), ["foo", "bar"]);
1423	}
1424
1425	#[test]
1426	fn test_wire_max_parts() {
1427		use crate::lite::Version;
1428
1429		let ok = (0..Path::MAX_PARTS)
1430			.map(|i| i.to_string())
1431			.collect::<Vec<_>>()
1432			.join("/");
1433		let too_deep = format!("{ok}/extra");
1434
1435		// Encode enforces the limit.
1436		let mut buf = bytes::BytesMut::new();
1437		Path::new(&ok).encode(&mut buf, Version::Lite04).unwrap();
1438		assert!(matches!(
1439			Path::new(&too_deep).encode(&mut bytes::BytesMut::new(), Version::Lite04),
1440			Err(EncodeError::BoundsExceeded)
1441		));
1442
1443		// Decode round-trips at the limit.
1444		let decoded = Path::decode(&mut buf.freeze(), Version::Lite04).unwrap();
1445		assert_eq!(decoded.as_str(), ok);
1446
1447		// Decode enforces the limit on a raw string that encode would have refused.
1448		let mut buf = bytes::BytesMut::new();
1449		too_deep.as_str().encode(&mut buf, Version::Lite04).unwrap();
1450		assert!(matches!(
1451			Path::decode(&mut buf.freeze(), Version::Lite04),
1452			Err(DecodeError::BoundsExceeded)
1453		));
1454	}
1455
1456	#[test]
1457	fn test_owned_empty_paths() {
1458		// Empty paths never allocate and stay well-behaved.
1459		let empty = Path::new("").to_owned();
1460		assert!(empty.is_empty());
1461		assert_eq!(empty, Path::empty());
1462
1463		let path = Path::new("foo").to_owned();
1464		let rest = path.strip_prefix("foo").unwrap().to_owned();
1465		assert!(rest.is_empty());
1466	}
1467
1468	#[test]
1469	fn test_prefix_list_canonical_order() {
1470		// Same inputs in different order produce identical results
1471		let a = PathPrefixes::new(["foo", "bar"]);
1472		let b = PathPrefixes::new(["bar", "foo"]);
1473		assert_eq!(a, b);
1474	}
1475
1476	#[test]
1477	fn test_path_relative_normalize() {
1478		assert_eq!(PathRelative::new("foo").as_str(), "foo");
1479		assert_eq!(PathRelative::new("/foo/").as_str(), "foo");
1480		assert_eq!(PathRelative::new("foo//bar").as_str(), "foo/bar");
1481		assert_eq!(PathRelative::new("../foo").as_str(), "../foo");
1482		assert_eq!(PathRelative::new("../../a/b").as_str(), "../../a/b");
1483		assert!(PathRelative::new("").is_empty());
1484	}
1485
1486	#[test]
1487	fn test_path_relative_normalizes_dot_segments() {
1488		assert_eq!(PathRelative::new(".").as_str(), ".");
1489		assert_eq!(PathRelative::new("././").as_str(), ".");
1490		assert_eq!(PathRelative::new("./foo").as_str(), "foo");
1491		assert_eq!(PathRelative::new("foo/./bar").as_str(), "foo/bar");
1492		assert_eq!(PathRelative::new("./../foo").as_str(), "../foo");
1493		// From<String> takes the same normalization.
1494		assert_eq!(PathRelative::from("./foo".to_string()).as_str(), "foo");
1495		assert_eq!(PathRelative::from(".".to_string()).as_str(), ".");
1496	}
1497
1498	#[test]
1499	fn test_resolve_replaces_base_name() {
1500		let base = Path::new("a/b");
1501		assert_eq!(base.resolve(&PathRelative::new("c")).as_str(), "a/c");
1502		assert_eq!(base.resolve(&PathRelative::new("c/d")).as_str(), "a/c/d");
1503		assert_eq!(
1504			Path::new("foo.hang/catalog.pro")
1505				.resolve(&PathRelative::new("./transcode.pro"))
1506				.as_str(),
1507			"foo.hang/transcode.pro"
1508		);
1509	}
1510
1511	#[test]
1512	fn test_resolve_empty_rel_returns_base() {
1513		let base = Path::new("a/b");
1514		assert_eq!(base.resolve(&PathRelative::new("")).as_str(), "a/b");
1515	}
1516
1517	#[test]
1518	fn test_resolve_single_dotdot() {
1519		let base = Path::new("a/b/c");
1520		assert_eq!(base.resolve(&PathRelative::new("../d")).as_str(), "a/d");
1521		assert_eq!(base.resolve(&PathRelative::new("..")).as_str(), "a");
1522	}
1523
1524	#[test]
1525	fn test_resolve_multiple_dotdot() {
1526		let base = Path::new("a/b/c");
1527		assert_eq!(base.resolve(&PathRelative::new("../../x")).as_str(), "x");
1528		assert_eq!(base.resolve(&PathRelative::new("../../../x")).as_str(), "x");
1529	}
1530
1531	#[test]
1532	fn test_resolve_dotdot_clamps_at_root() {
1533		let base = Path::new("a");
1534		// Excess `..` clamps at the root instead of escaping it.
1535		assert_eq!(base.resolve(&PathRelative::new("../../../foo")).as_str(), "foo");
1536		assert_eq!(base.resolve(&PathRelative::new("..")).as_str(), "");
1537	}
1538
1539	#[test]
1540	fn test_resolve_empty_base() {
1541		let base = Path::empty();
1542		assert_eq!(base.resolve(&PathRelative::new("foo")).as_str(), "foo");
1543		assert_eq!(base.resolve(&PathRelative::new("..")).as_str(), "");
1544	}
1545
1546	#[test]
1547	fn test_resolve_dot_names_parent() {
1548		let base = Path::new("a/b");
1549		assert_eq!(base.resolve(&PathRelative::new(".")).as_str(), "a");
1550		assert_eq!(base.resolve(&PathRelative::new("./c")).as_str(), "a/c");
1551		assert_eq!(base.resolve(&PathRelative::new("./../c")).as_str(), "c");
1552	}
1553
1554	#[test]
1555	fn test_resolve_self_reference_via_sibling_name() {
1556		// Naming the base within its parent yields the base unchanged, which lets the
1557		// caller compare resolved == base to detect a self-reference.
1558		let base = Path::new("a/b");
1559		assert_eq!(base.resolve(&PathRelative::new("./b")).as_str(), "a/b");
1560	}
1561
1562	#[test]
1563	fn test_try_resolve_distinguishes_root_from_escape() {
1564		let base = Path::new("top");
1565		assert_eq!(base.try_resolve(&PathRelative::new(".")).unwrap().as_str(), "");
1566		assert!(base.try_resolve(&PathRelative::new("..")).is_none());
1567
1568		let nested = Path::new("a/b");
1569		assert_eq!(nested.try_resolve(&PathRelative::new("..")).unwrap().as_str(), "");
1570		assert!(nested.try_resolve(&PathRelative::new("../..")).is_none());
1571	}
1572
1573	#[test]
1574	fn test_relative() {
1575		let rel = |target: &str, base: &str| Path::new(target).relative(base).unwrap();
1576
1577		// Nested under the base: the base's own last segment is replaced, so it repeats.
1578		assert_eq!(rel("foo/bar/baz", "foo/bar").as_str(), "bar/baz");
1579		// Sibling.
1580		assert_eq!(rel("foo/baz", "foo/bar").as_str(), "baz");
1581		// Different subtree.
1582		assert_eq!(rel("foo/baz/bar", "foo/bar/baz").as_str(), "../baz/bar");
1583		// The base's parent, which only `.` can name.
1584		assert_eq!(rel("a/b", "a/b/transcode.hang").as_str(), ".");
1585		assert_eq!(rel("a/b", "a/b/one/two/transcode.hang").as_str(), "../..");
1586		// Roots.
1587		assert_eq!(rel("foo/bar", "").as_str(), "foo/bar");
1588		assert_eq!(rel("", "foo").as_str(), ".");
1589		// The base itself, which only the empty reference names.
1590		assert_eq!(rel("a/b", "a/b").as_str(), "");
1591		assert_eq!(rel("", "").as_str(), "");
1592		// Slashes are normalized first.
1593		assert_eq!(rel("/a//b/", "//a/b/dir//").as_str(), ".");
1594	}
1595
1596	#[test]
1597	fn test_relative_rejects_unnameable_targets() {
1598		// A segment literally named `.` or `..` is a legal path component, but resolution
1599		// would walk on it instead of naming it.
1600		assert!(Path::new("a/../b").relative("").is_none());
1601		assert!(Path::new("x/./y").relative("x/z").is_none());
1602		assert!(Path::new("a/..").relative("a/b").is_none());
1603
1604		// A base is always nameable by itself, however its last segment is spelled.
1605		assert_eq!(Path::new("a/..").relative("a/..").unwrap().as_str(), "");
1606
1607		// Dot segments inside the shared prefix are never emitted, so they are fine.
1608		let rel = Path::new("a/../b/x").relative("a/../b/c").unwrap();
1609		assert_eq!(rel.as_str(), "x");
1610		assert_eq!(Path::new("a/../b/c").resolve(&rel).as_str(), "a/../b/x");
1611	}
1612
1613	#[test]
1614	fn test_relative_round_trips() {
1615		let paths = [
1616			"", "a", "b", "a/b", "a/c", "a/b/c", "a/b/c/d", "x/y/z", "a/../b", "a/./b", "a/..", "a/.",
1617		];
1618
1619		for base in paths {
1620			for target in paths {
1621				let base = Path::new(base);
1622				let target = Path::new(target);
1623				let Some(rel) = target.relative(&base) else {
1624					// Only an unnameable target may be refused, and never the base itself.
1625					assert!(
1626						target != base && target.parts().any(|part| part == "." || part == ".."),
1627						"{base} -> {target} refused a nameable target"
1628					);
1629					continue;
1630				};
1631
1632				assert_eq!(base.resolve(&rel), target, "{base} -> {target} via {rel}");
1633				// The reference is derived from a real target, so it never escapes the root.
1634				assert!(
1635					base.try_resolve(&rel).is_some(),
1636					"{base} -> {target} via {rel} escaped the root"
1637				);
1638			}
1639		}
1640	}
1641}