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
408// Comparisons, ordering, and hashing all go through `as_str()` so a borrowed and a
409// shared path with the same content behave identically (e.g. as map keys).
410impl<'b> PartialEq<Path<'b>> for Path<'_> {
411	fn eq(&self, other: &Path<'b>) -> bool {
412		self.as_str() == other.as_str()
413	}
414}
415
416impl Eq for Path<'_> {}
417
418impl PartialOrd for Path<'_> {
419	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
420		Some(self.cmp(other))
421	}
422}
423
424impl Ord for Path<'_> {
425	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
426		self.as_str().cmp(other.as_str())
427	}
428}
429
430impl std::hash::Hash for Path<'_> {
431	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
432		self.as_str().hash(state)
433	}
434}
435
436impl fmt::Debug for Path<'_> {
437	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438		f.debug_tuple("Path").field(&self.as_str()).finish()
439	}
440}
441
442impl serde::Serialize for Path<'_> {
443	fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
444		serializer.serialize_str(self.as_str())
445	}
446}
447
448impl<'a> From<&'a str> for Path<'a> {
449	fn from(s: &'a str) -> Self {
450		Self::new(s)
451	}
452}
453
454impl<'a> From<&'a String> for Path<'a> {
455	fn from(s: &'a String) -> Self {
456		// TODO avoid making a copy here
457		Self::new(s)
458	}
459}
460
461impl Default for Path<'_> {
462	fn default() -> Self {
463		Path::empty()
464	}
465}
466
467impl From<String> for Path<'_> {
468	fn from(s: String) -> Self {
469		Path::new(&s).into_owned()
470	}
471}
472
473impl AsRef<str> for Path<'_> {
474	fn as_ref(&self) -> &str {
475		self.as_str()
476	}
477}
478
479impl Display for Path<'_> {
480	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481		write!(f, "{}", self.as_str())
482	}
483}
484
485impl<V: Copy> Decode<V> for Path<'_>
486where
487	String: Decode<V>,
488{
489	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
490		let path: Path = String::decode(r, version)?.into();
491		if path.parts().count() > Path::MAX_PARTS {
492			return Err(DecodeError::BoundsExceeded);
493		}
494		Ok(path)
495	}
496}
497
498impl<V: Copy> Encode<V> for Path<'_>
499where
500	for<'a> &'a str: Encode<V>,
501{
502	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
503		if self.parts().count() > Path::MAX_PARTS {
504			return Err(EncodeError::BoundsExceeded);
505		}
506		self.as_str().encode(w, version)?;
507		Ok(())
508	}
509}
510
511/// An owned version of [`PathRelative`] with a `'static` lifetime.
512pub type PathRelativeOwned = PathRelative<'static>;
513
514/// A relative broadcast path, used to reference one broadcast from another broadcast's content.
515///
516/// Unlike [`Path`] (which is a complete reference within the broadcast namespace),
517/// `PathRelative` may contain `.` and `..` segments to walk the namespace and is meaningful
518/// only when resolved against a base [`Path`] via [`Path::resolve`]. The hang catalog uses it
519/// to point a rendition at a track published in a sibling broadcast (e.g. `./source`).
520///
521/// `PathRelative` has no `Encode`/`Decode` impl, so it never appears in announce/subscribe
522/// frames. It does serialize via serde for off-wire use (e.g. as a field inside a catalog
523/// JSON payload, which itself travels as a track).
524///
525/// Normalization on creation: leading/trailing slashes are trimmed, consecutive internal
526/// slashes collapse to one, and redundant `.` segments are stripped. A reference made only
527/// of `.` segments normalizes to `.` rather than empty because `.` resolves to the base's
528/// parent while empty resolves to the base itself. `..` is preserved for resolve time.
529///
530/// # Examples
531/// ```
532/// use moq_net::{Path, PathRelative};
533///
534/// let rel = PathRelative::new("./source");
535/// assert_eq!(Path::new("a/b").resolve(&rel).as_str(), "a/source");
536///
537/// // Redundant `.` segments are stripped on creation.
538/// assert_eq!(PathRelative::new("./a/./b").as_str(), "a/b");
539/// assert_eq!(PathRelative::new(".").as_str(), ".");
540/// ```
541#[derive(Debug, PartialEq, Eq, Hash, Clone, serde::Serialize)]
542pub struct PathRelative<'a>(Cow<'a, str>);
543
544impl<'a> PathRelative<'a> {
545	/// Create a new `PathRelative` from a string slice.
546	///
547	/// Leading and trailing slashes are trimmed, consecutive internal slashes collapse to one,
548	/// and redundant `.` segments are stripped. See the type-level doc for the full rules.
549	pub fn new(s: &'a str) -> Self {
550		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
551
552		if needs_normalize_relative(trimmed) {
553			Self(Cow::Owned(normalize_relative_segments(trimmed)))
554		} else {
555			Self(Cow::Borrowed(trimmed))
556		}
557	}
558
559	/// The normalized path as a string slice.
560	pub fn as_str(&self) -> &str {
561		&self.0
562	}
563
564	/// The empty relative path, which resolves to the base path itself.
565	pub fn empty() -> PathRelative<'static> {
566		PathRelative(Cow::Borrowed(""))
567	}
568
569	/// True if the path is empty (resolves to the base path itself).
570	pub fn is_empty(&self) -> bool {
571		self.0.is_empty()
572	}
573
574	/// The length of the normalized path in bytes.
575	pub fn len(&self) -> usize {
576		self.0.len()
577	}
578
579	/// Copy into an owned version with a `'static` lifetime.
580	pub fn to_owned(&self) -> PathRelativeOwned {
581		PathRelative(Cow::Owned(self.0.to_string()))
582	}
583
584	/// Convert into an owned version with a `'static` lifetime.
585	pub fn into_owned(self) -> PathRelativeOwned {
586		PathRelative(Cow::Owned(self.0.into_owned()))
587	}
588
589	/// Reborrow without copying.
590	pub fn borrow(&'a self) -> PathRelative<'a> {
591		PathRelative(Cow::Borrowed(&self.0))
592	}
593}
594
595impl<'a> From<&'a str> for PathRelative<'a> {
596	fn from(s: &'a str) -> Self {
597		Self::new(s)
598	}
599}
600
601impl<'a> From<&'a String> for PathRelative<'a> {
602	fn from(s: &'a String) -> Self {
603		Self::new(s)
604	}
605}
606
607impl From<String> for PathRelative<'_> {
608	fn from(s: String) -> Self {
609		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
610
611		if needs_normalize_relative(trimmed) {
612			Self(Cow::Owned(normalize_relative_segments(trimmed)))
613		} else if trimmed == s {
614			Self(Cow::Owned(s))
615		} else {
616			Self(Cow::Owned(trimmed.to_string()))
617		}
618	}
619}
620
621fn needs_normalize_relative(trimmed: &str) -> bool {
622	trimmed.split('/').any(|seg| seg.is_empty() || seg == ".")
623}
624
625fn normalize_relative_segments(trimmed: &str) -> String {
626	let segments = trimmed
627		.split('/')
628		.filter(|seg| !seg.is_empty() && *seg != ".")
629		.collect::<Vec<_>>()
630		.join("/");
631
632	if segments.is_empty() && trimmed.split('/').any(|seg| seg == ".") {
633		".".to_string()
634	} else {
635		segments
636	}
637}
638
639impl Default for PathRelative<'_> {
640	fn default() -> Self {
641		Self(Cow::Borrowed(""))
642	}
643}
644
645impl AsRef<str> for PathRelative<'_> {
646	fn as_ref(&self) -> &str {
647		&self.0
648	}
649}
650
651impl Display for PathRelative<'_> {
652	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653		write!(f, "{}", self.0)
654	}
655}
656
657// Owned-only deserialization. We use `String::deserialize` so that owned deserializers
658// (e.g. `serde_json::from_slice`) work. The borrowed form `<&str>::deserialize` requires
659// `'de: 'a`, which is unsatisfiable when `'a = 'static`.
660impl<'de> serde::Deserialize<'de> for PathRelative<'static> {
661	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
662	where
663		D: serde::Deserializer<'de>,
664	{
665		let s = String::deserialize(deserializer)?;
666		Ok(PathRelative::from(s))
667	}
668}
669
670/// A deduplicated list of path prefixes.
671///
672/// Automatically removes exact duplicates and overlapping prefixes on construction.
673/// For example, `["demo", "demo/foo", "anon"]` becomes `["demo", "anon"]` since
674/// `"demo"` already covers `"demo/foo"`.
675#[derive(Debug, Clone, Default, Eq)]
676pub struct PathPrefixes {
677	paths: Vec<PathOwned>,
678}
679
680impl PathPrefixes {
681	/// Create a new PathPrefixes, deduplicating and removing overlapping prefixes.
682	///
683	/// Shorter prefixes subsume longer ones: `"demo"` covers `"demo/foo"`.
684	///
685	/// Accepts anything iterable over path-like items:
686	/// ```
687	/// use moq_net::PathPrefixes;
688	///
689	/// let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
690	/// assert_eq!(list.len(), 2); // "demo/foo" subsumed by "demo"
691	/// ```
692	pub fn new(paths: impl IntoIterator<Item = impl AsPath>) -> Self {
693		let mut paths: Vec<PathOwned> = paths.into_iter().map(|p| p.as_path().to_owned()).collect();
694
695		if paths.len() <= 1 {
696			return Self { paths };
697		}
698
699		// Sort by length so shorter (more permissive) prefixes come first.
700		// Tie-break lexicographically for canonical ordering.
701		paths.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.as_str().cmp(b.as_str())));
702		paths.dedup();
703
704		let mut result: Vec<PathOwned> = Vec::new();
705		'outer: for path in paths {
706			for existing in &result {
707				if path.has_prefix(existing) {
708					continue 'outer;
709				}
710			}
711			result.push(path);
712		}
713
714		Self { paths: result }
715	}
716
717	/// Returns `true` if the set contains no prefixes, so it matches nothing.
718	pub fn is_empty(&self) -> bool {
719		self.paths.is_empty()
720	}
721
722	/// The number of prefixes, after redundant ones were collapsed.
723	pub fn len(&self) -> usize {
724		self.paths.len()
725	}
726
727	/// Iterate the prefixes in the set.
728	pub fn iter(&self) -> std::slice::Iter<'_, PathOwned> {
729		self.paths.iter()
730	}
731}
732
733impl std::ops::Deref for PathPrefixes {
734	type Target = [PathOwned];
735
736	fn deref(&self) -> &[PathOwned] {
737		&self.paths
738	}
739}
740
741impl FromIterator<PathOwned> for PathPrefixes {
742	fn from_iter<I: IntoIterator<Item = PathOwned>>(iter: I) -> Self {
743		Self::new(iter)
744	}
745}
746
747impl From<Vec<PathOwned>> for PathPrefixes {
748	fn from(paths: Vec<PathOwned>) -> Self {
749		Self::new(paths)
750	}
751}
752
753impl<'a> PartialEq<Vec<Path<'a>>> for PathPrefixes {
754	fn eq(&self, other: &Vec<Path<'a>>) -> bool {
755		self.paths == *other
756	}
757}
758
759impl<'a> PartialEq<PathPrefixes> for Vec<Path<'a>> {
760	fn eq(&self, other: &PathPrefixes) -> bool {
761		*self == other.paths
762	}
763}
764
765impl PartialEq for PathPrefixes {
766	fn eq(&self, other: &Self) -> bool {
767		self.paths == other.paths
768	}
769}
770
771impl IntoIterator for PathPrefixes {
772	type Item = PathOwned;
773	type IntoIter = std::vec::IntoIter<PathOwned>;
774
775	fn into_iter(self) -> Self::IntoIter {
776		self.paths.into_iter()
777	}
778}
779
780impl<'a> IntoIterator for &'a PathPrefixes {
781	type Item = &'a PathOwned;
782	type IntoIter = std::slice::Iter<'a, PathOwned>;
783
784	fn into_iter(self) -> Self::IntoIter {
785		self.paths.iter()
786	}
787}
788
789#[cfg(test)]
790mod tests {
791	use super::*;
792
793	#[test]
794	fn test_has_prefix() {
795		let path = Path::new("foo/bar/baz");
796
797		// Valid prefixes - test with both &str and &Path
798		assert!(path.has_prefix(""));
799		assert!(path.has_prefix("foo"));
800		assert!(path.has_prefix(Path::new("foo")));
801		assert!(path.has_prefix("foo/"));
802		assert!(path.has_prefix("foo/bar"));
803		assert!(path.has_prefix(Path::new("foo/bar/")));
804		assert!(path.has_prefix("foo/bar/baz"));
805
806		// Invalid prefixes - should not match partial components
807		assert!(!path.has_prefix("f"));
808		assert!(!path.has_prefix(Path::new("fo")));
809		assert!(!path.has_prefix("foo/b"));
810		assert!(!path.has_prefix("foo/ba"));
811		assert!(!path.has_prefix(Path::new("foo/bar/ba")));
812
813		// Edge case: "foobar" should not match "foo"
814		let path = Path::new("foobar");
815		assert!(!path.has_prefix("foo"));
816		assert!(path.has_prefix(Path::new("foobar")));
817	}
818
819	#[test]
820	fn test_strip_prefix() {
821		let path = Path::new("foo/bar/baz");
822
823		// Test with both &str and &Path
824		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
825		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
826		assert_eq!(path.strip_prefix(Path::new("foo/")).unwrap().as_str(), "bar/baz");
827		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
828		assert_eq!(path.strip_prefix(Path::new("foo/bar/")).unwrap().as_str(), "baz");
829		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
830
831		// Should fail for invalid prefixes
832		assert!(path.strip_prefix("fo").is_none());
833		assert!(path.strip_prefix(Path::new("bar")).is_none());
834	}
835
836	#[test]
837	fn test_join() {
838		// Test with both &str and &Path
839		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
840		assert_eq!(Path::new("foo/").join(Path::new("bar")).as_str(), "foo/bar");
841		assert_eq!(Path::new("").join("bar").as_str(), "bar");
842		assert_eq!(Path::new("foo/bar").join(Path::new("baz")).as_str(), "foo/bar/baz");
843	}
844
845	#[test]
846	fn test_empty() {
847		let empty = Path::new("");
848		assert!(empty.is_empty());
849		assert_eq!(empty.len(), 0);
850
851		let non_empty = Path::new("foo");
852		assert!(!non_empty.is_empty());
853		assert_eq!(non_empty.len(), 3);
854	}
855
856	#[test]
857	fn test_from_conversions() {
858		let path1 = Path::from("foo/bar");
859		let path2 = Path::from("foo/bar");
860		let s = String::from("foo/bar");
861		let path3 = Path::from(&s);
862
863		assert_eq!(path1.as_str(), "foo/bar");
864		assert_eq!(path2.as_str(), "foo/bar");
865		assert_eq!(path3.as_str(), "foo/bar");
866	}
867
868	#[test]
869	fn test_path_prefix_join() {
870		let prefix = Path::new("foo");
871		let suffix = Path::new("bar/baz");
872		let path = prefix.join(&suffix);
873		assert_eq!(path.as_str(), "foo/bar/baz");
874
875		let prefix = Path::new("foo/");
876		let suffix = Path::new("bar/baz");
877		let path = prefix.join(&suffix);
878		assert_eq!(path.as_str(), "foo/bar/baz");
879
880		let prefix = Path::new("foo");
881		let suffix = Path::new("/bar/baz");
882		let path = prefix.join(&suffix);
883		assert_eq!(path.as_str(), "foo/bar/baz");
884
885		let prefix = Path::new("");
886		let suffix = Path::new("bar/baz");
887		let path = prefix.join(&suffix);
888		assert_eq!(path.as_str(), "bar/baz");
889	}
890
891	#[test]
892	fn test_path_prefix_conversions() {
893		let prefix1 = Path::from("foo/bar");
894		let prefix2 = Path::from(String::from("foo/bar"));
895		let s = String::from("foo/bar");
896		let prefix3 = Path::from(&s);
897
898		assert_eq!(prefix1.as_str(), "foo/bar");
899		assert_eq!(prefix2.as_str(), "foo/bar");
900		assert_eq!(prefix3.as_str(), "foo/bar");
901	}
902
903	#[test]
904	fn test_path_suffix_conversions() {
905		let suffix1 = Path::from("foo/bar");
906		let suffix2 = Path::from(String::from("foo/bar"));
907		let s = String::from("foo/bar");
908		let suffix3 = Path::from(&s);
909
910		assert_eq!(suffix1.as_str(), "foo/bar");
911		assert_eq!(suffix2.as_str(), "foo/bar");
912		assert_eq!(suffix3.as_str(), "foo/bar");
913	}
914
915	#[test]
916	fn test_path_types_basic_operations() {
917		let prefix = Path::new("foo/bar");
918		assert_eq!(prefix.as_str(), "foo/bar");
919		assert!(!prefix.is_empty());
920		assert_eq!(prefix.len(), 7);
921
922		let suffix = Path::new("baz/qux");
923		assert_eq!(suffix.as_str(), "baz/qux");
924		assert!(!suffix.is_empty());
925		assert_eq!(suffix.len(), 7);
926
927		let empty_prefix = Path::new("");
928		assert!(empty_prefix.is_empty());
929		assert_eq!(empty_prefix.len(), 0);
930
931		let empty_suffix = Path::new("");
932		assert!(empty_suffix.is_empty());
933		assert_eq!(empty_suffix.len(), 0);
934	}
935
936	#[test]
937	fn test_prefix_has_prefix() {
938		// Test empty prefix (should match everything)
939		let prefix = Path::new("foo/bar");
940		assert!(prefix.has_prefix(""));
941
942		// Test exact matches
943		let prefix = Path::new("foo/bar");
944		assert!(prefix.has_prefix("foo/bar"));
945
946		// Test valid prefixes
947		assert!(prefix.has_prefix("foo"));
948		assert!(prefix.has_prefix("foo/"));
949
950		// Test invalid prefixes - partial matches should fail
951		assert!(!prefix.has_prefix("f"));
952		assert!(!prefix.has_prefix("fo"));
953		assert!(!prefix.has_prefix("foo/b"));
954		assert!(!prefix.has_prefix("foo/ba"));
955
956		// Test edge cases
957		let prefix = Path::new("foobar");
958		assert!(!prefix.has_prefix("foo"));
959		assert!(prefix.has_prefix("foobar"));
960
961		// Test trailing slash handling
962		let prefix = Path::new("foo/bar/");
963		assert!(prefix.has_prefix("foo"));
964		assert!(prefix.has_prefix("foo/"));
965		assert!(prefix.has_prefix("foo/bar"));
966		assert!(prefix.has_prefix("foo/bar/"));
967
968		// Test single component
969		let prefix = Path::new("foo");
970		assert!(prefix.has_prefix(""));
971		assert!(prefix.has_prefix("foo"));
972		assert!(prefix.has_prefix("foo/")); // "foo/" becomes "foo" after trimming
973		assert!(!prefix.has_prefix("f"));
974
975		// Test empty prefix
976		let prefix = Path::new("");
977		assert!(prefix.has_prefix(""));
978		assert!(!prefix.has_prefix("foo"));
979	}
980
981	#[test]
982	fn test_prefix_join() {
983		// Basic joining
984		let prefix = Path::new("foo");
985		let suffix = Path::new("bar");
986		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
987
988		// Trailing slash on prefix
989		let prefix = Path::new("foo/");
990		let suffix = Path::new("bar");
991		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
992
993		// Leading slash on suffix
994		let prefix = Path::new("foo");
995		let suffix = Path::new("/bar");
996		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
997
998		// Trailing slash on suffix
999		let prefix = Path::new("foo");
1000		let suffix = Path::new("bar/");
1001		assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); // trailing slash is trimmed
1002
1003		// Both have slashes
1004		let prefix = Path::new("foo/");
1005		let suffix = Path::new("/bar");
1006		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
1007
1008		// Empty suffix
1009		let prefix = Path::new("foo");
1010		let suffix = Path::new("");
1011		assert_eq!(prefix.join(suffix).as_str(), "foo");
1012
1013		// Empty prefix
1014		let prefix = Path::new("");
1015		let suffix = Path::new("bar");
1016		assert_eq!(prefix.join(suffix).as_str(), "bar");
1017
1018		// Both empty
1019		let prefix = Path::new("");
1020		let suffix = Path::new("");
1021		assert_eq!(prefix.join(suffix).as_str(), "");
1022
1023		// Complex paths
1024		let prefix = Path::new("foo/bar");
1025		let suffix = Path::new("baz/qux");
1026		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux");
1027
1028		// Complex paths with slashes
1029		let prefix = Path::new("foo/bar/");
1030		let suffix = Path::new("/baz/qux/");
1031		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
1032	}
1033
1034	#[test]
1035	fn test_path_ref() {
1036		// Test PathRef creation and normalization
1037		let ref1 = Path::new("/foo/bar/");
1038		assert_eq!(ref1.as_str(), "foo/bar");
1039
1040		let ref2 = Path::from("///foo///");
1041		assert_eq!(ref2.as_str(), "foo");
1042
1043		// Test PathRef normalizes multiple slashes
1044		let ref3 = Path::new("foo//bar///baz");
1045		assert_eq!(ref3.as_str(), "foo/bar/baz");
1046
1047		// Test conversions
1048		let path = Path::new("foo/bar");
1049		let path_ref = path;
1050		assert_eq!(path_ref.as_str(), "foo/bar");
1051
1052		// Test that Path methods work with PathRef
1053		let path2 = Path::new("foo/bar/baz");
1054		assert!(path2.has_prefix(&path_ref));
1055		assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz");
1056
1057		// Test empty PathRef
1058		let empty = Path::new("");
1059		assert!(empty.is_empty());
1060		assert_eq!(empty.len(), 0);
1061	}
1062
1063	#[test]
1064	fn test_multiple_consecutive_slashes() {
1065		let path = Path::new("foo//bar///baz");
1066		// Multiple consecutive slashes are collapsed to single slashes
1067		assert_eq!(path.as_str(), "foo/bar/baz");
1068
1069		// Test with leading and trailing slashes too
1070		let path2 = Path::new("//foo//bar///baz//");
1071		assert_eq!(path2.as_str(), "foo/bar/baz");
1072
1073		// Test empty segments are handled correctly
1074		let path3 = Path::new("foo///bar");
1075		assert_eq!(path3.as_str(), "foo/bar");
1076	}
1077
1078	#[test]
1079	fn test_removes_multiple_slashes_comprehensively() {
1080		// Test various multiple slash scenarios
1081		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
1082		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
1083		assert_eq!(Path::new("foo////bar").as_str(), "foo/bar");
1084
1085		// Multiple occurrences of double slashes
1086		assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz");
1087		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
1088
1089		// Mixed slash counts
1090		assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux");
1091
1092		// With leading and trailing slashes
1093		assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar");
1094		assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar");
1095
1096		// Edge case: only slashes
1097		assert_eq!(Path::new("//").as_str(), "");
1098		assert_eq!(Path::new("////").as_str(), "");
1099
1100		// Test that operations work correctly with normalized paths
1101		let path_with_slashes = Path::new("foo//bar///baz");
1102		assert!(path_with_slashes.has_prefix("foo/bar"));
1103		assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz");
1104		assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux");
1105
1106		// Test PathRef to Path conversion
1107		let path_ref = Path::new("foo//bar///baz");
1108		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too
1109		let path_from_ref = path_ref.to_owned();
1110		assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized
1111	}
1112
1113	#[test]
1114	fn test_path_ref_multiple_slashes() {
1115		// PathRef now normalizes multiple slashes using Cow
1116		let path_ref = Path::new("//foo//bar///baz//");
1117		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized
1118
1119		// Various multiple slash scenarios are normalized in PathRef
1120		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
1121		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
1122		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
1123
1124		// Conversion to Path maintains normalized form
1125		assert_eq!(Path::new("foo//bar").to_owned().as_str(), "foo/bar");
1126		assert_eq!(Path::new("foo///bar").to_owned().as_str(), "foo/bar");
1127		assert_eq!(Path::new("a//b//c//d").to_owned().as_str(), "a/b/c/d");
1128
1129		// Edge cases
1130		assert_eq!(Path::new("//").as_str(), "");
1131		assert_eq!(Path::new("////").as_str(), "");
1132		assert_eq!(Path::new("//").to_owned().as_str(), "");
1133		assert_eq!(Path::new("////").to_owned().as_str(), "");
1134
1135		// Test that PathRef avoids allocation when no normalization needed
1136		let normal_path = Path::new("foo/bar/baz");
1137		assert_eq!(normal_path.as_str(), "foo/bar/baz");
1138		// This should use Cow::Borrowed internally (no allocation)
1139
1140		let needs_norm = Path::new("foo//bar");
1141		assert_eq!(needs_norm.as_str(), "foo/bar");
1142		// This should use Cow::Owned internally (allocation only when needed)
1143	}
1144
1145	#[test]
1146	fn test_ergonomic_conversions() {
1147		// Test that all these work ergonomically in function calls
1148		fn takes_path_ref<'a>(p: impl Into<Path<'a>>) -> String {
1149			p.into().as_str().to_string()
1150		}
1151
1152		// Alternative API using the trait alias for better error messages
1153		fn takes_path_ref_with_trait<'a>(p: impl Into<Path<'a>>) -> String {
1154			p.into().as_str().to_string()
1155		}
1156
1157		// String literal
1158		assert_eq!(takes_path_ref("foo//bar"), "foo/bar");
1159
1160		// String (owned) - this should now work without &
1161		let owned_string = String::from("foo//bar///baz");
1162		assert_eq!(takes_path_ref(owned_string), "foo/bar/baz");
1163
1164		// &String
1165		let string_ref = String::from("foo//bar");
1166		assert_eq!(takes_path_ref(string_ref), "foo/bar");
1167
1168		// PathRef
1169		let path_ref = Path::new("foo//bar");
1170		assert_eq!(takes_path_ref(path_ref), "foo/bar");
1171
1172		// Path
1173		let path = Path::new("foo//bar");
1174		assert_eq!(takes_path_ref(path), "foo/bar");
1175
1176		// Test that Path::new works with all these types
1177		let _path1 = Path::new("foo/bar"); // &str
1178		let _path2 = Path::new("foo/bar"); // String - should now work
1179		let _path3 = Path::new("foo/bar"); // &String
1180		let _path4 = Path::new("foo/bar"); // PathRef
1181
1182		// Test the trait alias version works the same
1183		assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar");
1184		assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar");
1185	}
1186
1187	#[test]
1188	fn test_prefix_strip_prefix() {
1189		// Test basic stripping
1190		let prefix = Path::new("foo/bar/baz");
1191		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
1192		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar/baz");
1193		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar/baz");
1194		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "baz");
1195		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "baz");
1196		assert_eq!(prefix.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
1197
1198		// Test invalid prefixes
1199		assert!(prefix.strip_prefix("fo").is_none());
1200		assert!(prefix.strip_prefix("bar").is_none());
1201		assert!(prefix.strip_prefix("foo/ba").is_none());
1202
1203		// Test edge cases
1204		let prefix = Path::new("foobar");
1205		assert!(prefix.strip_prefix("foo").is_none());
1206		assert_eq!(prefix.strip_prefix("foobar").unwrap().as_str(), "");
1207
1208		// Test empty prefix
1209		let prefix = Path::new("");
1210		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "");
1211		assert!(prefix.strip_prefix("foo").is_none());
1212
1213		// Test single component
1214		let prefix = Path::new("foo");
1215		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "");
1216		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
1217
1218		// Test trailing slash handling
1219		let prefix = Path::new("foo/bar/");
1220		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar");
1221		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar");
1222		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "");
1223		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "");
1224	}
1225
1226	#[test]
1227	fn test_prefix_list_dedup() {
1228		// Exact duplicates are removed
1229		let list = PathPrefixes::new(["demo", "demo"]);
1230		assert_eq!(list.len(), 1);
1231		assert_eq!(list[0], Path::new("demo"));
1232	}
1233
1234	#[test]
1235	fn test_prefix_list_overlap() {
1236		// "demo/foo" is redundant when "demo" exists
1237		let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
1238		assert_eq!(list.len(), 2);
1239		assert!(list.iter().any(|p| p == &Path::new("demo")));
1240		assert!(list.iter().any(|p| p == &Path::new("anon")));
1241	}
1242
1243	#[test]
1244	fn test_prefix_list_overlap_reverse_order() {
1245		// Order shouldn't matter
1246		let list = PathPrefixes::new(["demo/foo", "demo"]);
1247		assert_eq!(list.len(), 1);
1248		assert_eq!(list[0], Path::new("demo"));
1249	}
1250
1251	#[test]
1252	fn test_prefix_list_empty_covers_all() {
1253		// Empty prefix covers everything
1254		let list = PathPrefixes::new(["", "demo", "anon"]);
1255		assert_eq!(list.len(), 1);
1256		assert_eq!(list[0], Path::new(""));
1257	}
1258
1259	#[test]
1260	fn test_prefix_list_no_overlap() {
1261		// Unrelated prefixes are all kept
1262		let list = PathPrefixes::new(["demo", "anon", "secret"]);
1263		assert_eq!(list.len(), 3);
1264	}
1265
1266	#[test]
1267	fn test_prefix_list_single() {
1268		let list = PathPrefixes::new(["demo"]);
1269		assert_eq!(list.len(), 1);
1270	}
1271
1272	#[test]
1273	fn test_prefix_list_empty() {
1274		let list = PathPrefixes::new(std::iter::empty::<&str>());
1275		assert!(list.is_empty());
1276		assert_eq!(list.len(), 0);
1277	}
1278
1279	#[test]
1280	fn test_prefix_list_deep_overlap() {
1281		// "a/b/c" is covered by "a/b" which is covered by "a"
1282		let list = PathPrefixes::new(["a/b/c", "a/b", "a"]);
1283		assert_eq!(list.len(), 1);
1284		assert_eq!(list[0], Path::new("a"));
1285	}
1286
1287	#[test]
1288	fn test_prefix_list_partial_name_not_overlap() {
1289		// "demo" should NOT cover "demonstration" (different path component)
1290		let list = PathPrefixes::new(["demo", "demonstration"]);
1291		assert_eq!(list.len(), 2);
1292	}
1293
1294	#[test]
1295	fn test_prefix_list_collect() {
1296		let paths: Vec<PathOwned> = vec!["demo".into(), "demo/foo".into()];
1297		let list: PathPrefixes = paths.into_iter().collect();
1298		assert_eq!(list.len(), 1);
1299		assert_eq!(list[0], Path::new("demo"));
1300	}
1301
1302	#[test]
1303	fn test_prefix_list_eq_vec() {
1304		let list = PathPrefixes::new(["demo", "anon"]);
1305		// Canonical order: sorted by length, then lexicographically
1306		assert_eq!(list, vec!["anon".as_path(), "demo".as_path()]);
1307	}
1308
1309	// Pointer-equality checks that owned paths share one allocation through the
1310	// clone / to_owned / strip_prefix flow used by origin announce fan-out.
1311	#[test]
1312	fn test_owned_paths_share_allocation() {
1313		let path = Path::new("customer/room/broadcast").to_owned();
1314
1315		// Cloning an owned path shares the buffer.
1316		let cloned = path.clone();
1317		assert_eq!(path.as_str().as_ptr(), cloned.as_str().as_ptr());
1318
1319		// as_path + to_owned (how notify queues a path per consumer) shares too.
1320		let requeued = path.as_path().to_owned();
1321		assert_eq!(path.as_str().as_ptr(), requeued.as_str().as_ptr());
1322
1323		// Stripping a prefix from an owned path is offset arithmetic, not a copy.
1324		let stripped = path.strip_prefix("customer").unwrap().to_owned();
1325		assert_eq!(stripped.as_str(), "room/broadcast");
1326		assert_eq!(stripped.as_str().as_ptr(), path.as_str()["customer/".len()..].as_ptr());
1327
1328		// next_part shares the rest as well.
1329		let (dir, rest) = path.next_part().unwrap();
1330		assert_eq!(dir, "customer");
1331		let rest = rest.to_owned();
1332		assert_eq!(rest.as_str().as_ptr(), stripped.as_str().as_ptr());
1333
1334		// join produces an owned path whose clones share.
1335		let joined = path.join("alice");
1336		let joined2 = joined.clone();
1337		assert_eq!(joined.as_str(), "customer/room/broadcast/alice");
1338		assert_eq!(joined.as_str().as_ptr(), joined2.as_str().as_ptr());
1339	}
1340
1341	#[test]
1342	fn test_parts() {
1343		assert_eq!(Path::empty().parts().count(), 0);
1344		assert_eq!(Path::new("foo").parts().collect::<Vec<_>>(), ["foo"]);
1345		assert_eq!(Path::new("/foo//bar/").parts().collect::<Vec<_>>(), ["foo", "bar"]);
1346	}
1347
1348	#[test]
1349	fn test_wire_max_parts() {
1350		use crate::lite::Version;
1351
1352		let ok = (0..Path::MAX_PARTS)
1353			.map(|i| i.to_string())
1354			.collect::<Vec<_>>()
1355			.join("/");
1356		let too_deep = format!("{ok}/extra");
1357
1358		// Encode enforces the limit.
1359		let mut buf = bytes::BytesMut::new();
1360		Path::new(&ok).encode(&mut buf, Version::Lite04).unwrap();
1361		assert!(matches!(
1362			Path::new(&too_deep).encode(&mut bytes::BytesMut::new(), Version::Lite04),
1363			Err(EncodeError::BoundsExceeded)
1364		));
1365
1366		// Decode round-trips at the limit.
1367		let decoded = Path::decode(&mut buf.freeze(), Version::Lite04).unwrap();
1368		assert_eq!(decoded.as_str(), ok);
1369
1370		// Decode enforces the limit on a raw string that encode would have refused.
1371		let mut buf = bytes::BytesMut::new();
1372		too_deep.as_str().encode(&mut buf, Version::Lite04).unwrap();
1373		assert!(matches!(
1374			Path::decode(&mut buf.freeze(), Version::Lite04),
1375			Err(DecodeError::BoundsExceeded)
1376		));
1377	}
1378
1379	#[test]
1380	fn test_owned_empty_paths() {
1381		// Empty paths never allocate and stay well-behaved.
1382		let empty = Path::new("").to_owned();
1383		assert!(empty.is_empty());
1384		assert_eq!(empty, Path::empty());
1385
1386		let path = Path::new("foo").to_owned();
1387		let rest = path.strip_prefix("foo").unwrap().to_owned();
1388		assert!(rest.is_empty());
1389	}
1390
1391	#[test]
1392	fn test_prefix_list_canonical_order() {
1393		// Same inputs in different order produce identical results
1394		let a = PathPrefixes::new(["foo", "bar"]);
1395		let b = PathPrefixes::new(["bar", "foo"]);
1396		assert_eq!(a, b);
1397	}
1398
1399	#[test]
1400	fn test_path_relative_normalize() {
1401		assert_eq!(PathRelative::new("foo").as_str(), "foo");
1402		assert_eq!(PathRelative::new("/foo/").as_str(), "foo");
1403		assert_eq!(PathRelative::new("foo//bar").as_str(), "foo/bar");
1404		assert_eq!(PathRelative::new("../foo").as_str(), "../foo");
1405		assert_eq!(PathRelative::new("../../a/b").as_str(), "../../a/b");
1406		assert!(PathRelative::new("").is_empty());
1407	}
1408
1409	#[test]
1410	fn test_path_relative_normalizes_dot_segments() {
1411		assert_eq!(PathRelative::new(".").as_str(), ".");
1412		assert_eq!(PathRelative::new("././").as_str(), ".");
1413		assert_eq!(PathRelative::new("./foo").as_str(), "foo");
1414		assert_eq!(PathRelative::new("foo/./bar").as_str(), "foo/bar");
1415		assert_eq!(PathRelative::new("./../foo").as_str(), "../foo");
1416		// From<String> takes the same normalization.
1417		assert_eq!(PathRelative::from("./foo".to_string()).as_str(), "foo");
1418		assert_eq!(PathRelative::from(".".to_string()).as_str(), ".");
1419	}
1420
1421	#[test]
1422	fn test_resolve_replaces_base_name() {
1423		let base = Path::new("a/b");
1424		assert_eq!(base.resolve(&PathRelative::new("c")).as_str(), "a/c");
1425		assert_eq!(base.resolve(&PathRelative::new("c/d")).as_str(), "a/c/d");
1426		assert_eq!(
1427			Path::new("foo.hang/catalog.pro")
1428				.resolve(&PathRelative::new("./transcode.pro"))
1429				.as_str(),
1430			"foo.hang/transcode.pro"
1431		);
1432	}
1433
1434	#[test]
1435	fn test_resolve_empty_rel_returns_base() {
1436		let base = Path::new("a/b");
1437		assert_eq!(base.resolve(&PathRelative::new("")).as_str(), "a/b");
1438	}
1439
1440	#[test]
1441	fn test_resolve_single_dotdot() {
1442		let base = Path::new("a/b/c");
1443		assert_eq!(base.resolve(&PathRelative::new("../d")).as_str(), "a/d");
1444		assert_eq!(base.resolve(&PathRelative::new("..")).as_str(), "a");
1445	}
1446
1447	#[test]
1448	fn test_resolve_multiple_dotdot() {
1449		let base = Path::new("a/b/c");
1450		assert_eq!(base.resolve(&PathRelative::new("../../x")).as_str(), "x");
1451		assert_eq!(base.resolve(&PathRelative::new("../../../x")).as_str(), "x");
1452	}
1453
1454	#[test]
1455	fn test_resolve_dotdot_clamps_at_root() {
1456		let base = Path::new("a");
1457		// Excess `..` clamps at the root instead of escaping it.
1458		assert_eq!(base.resolve(&PathRelative::new("../../../foo")).as_str(), "foo");
1459		assert_eq!(base.resolve(&PathRelative::new("..")).as_str(), "");
1460	}
1461
1462	#[test]
1463	fn test_resolve_empty_base() {
1464		let base = Path::empty();
1465		assert_eq!(base.resolve(&PathRelative::new("foo")).as_str(), "foo");
1466		assert_eq!(base.resolve(&PathRelative::new("..")).as_str(), "");
1467	}
1468
1469	#[test]
1470	fn test_resolve_dot_names_parent() {
1471		let base = Path::new("a/b");
1472		assert_eq!(base.resolve(&PathRelative::new(".")).as_str(), "a");
1473		assert_eq!(base.resolve(&PathRelative::new("./c")).as_str(), "a/c");
1474		assert_eq!(base.resolve(&PathRelative::new("./../c")).as_str(), "c");
1475	}
1476
1477	#[test]
1478	fn test_resolve_self_reference_via_sibling_name() {
1479		// Naming the base within its parent yields the base unchanged, which lets the
1480		// caller compare resolved == base to detect a self-reference.
1481		let base = Path::new("a/b");
1482		assert_eq!(base.resolve(&PathRelative::new("./b")).as_str(), "a/b");
1483	}
1484
1485	#[test]
1486	fn test_try_resolve_distinguishes_root_from_escape() {
1487		let base = Path::new("top");
1488		assert_eq!(base.try_resolve(&PathRelative::new(".")).unwrap().as_str(), "");
1489		assert!(base.try_resolve(&PathRelative::new("..")).is_none());
1490
1491		let nested = Path::new("a/b");
1492		assert_eq!(nested.try_resolve(&PathRelative::new("..")).unwrap().as_str(), "");
1493		assert!(nested.try_resolve(&PathRelative::new("../..")).is_none());
1494	}
1495}