Skip to main content

moq_net/path/
mod.rs

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