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