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