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