Skip to main content

moq_net/
path.rs

1use std::fmt::{self, Display};
2use std::sync::Arc;
3
4use crate::coding::{Decode, DecodeError, Encode, EncodeError};
5
6/// An owned version of [`Path`] with a `'static` lifetime.
7pub type PathOwned = Path<'static>;
8
9/// A trait for types that can be converted to a `Path`.
10///
11/// When providing a String/str, any leading/trailing slashes are trimmed and multiple consecutive slashes are collapsed.
12/// When already a Path, normalization is skipped and the underlying buffer is reused without copying.
13pub trait AsPath {
14	fn as_path(&self) -> Path<'_>;
15}
16
17impl<'a> AsPath for &'a str {
18	fn as_path(&self) -> Path<'a> {
19		Path::new(self)
20	}
21}
22
23impl<'a> AsPath for &'a Path<'a> {
24	fn as_path(&self) -> Path<'a> {
25		// We don't normalize again nor do we copy the bytes.
26		self.borrow()
27	}
28}
29
30impl AsPath for Path<'_> {
31	fn as_path(&self) -> Path<'_> {
32		self.borrow()
33	}
34}
35
36impl AsPath for String {
37	fn as_path(&self) -> Path<'_> {
38		Path::new(self)
39	}
40}
41
42impl<'a> AsPath for &'a String {
43	fn as_path(&self) -> Path<'a> {
44		Path::new(self)
45	}
46}
47
48/// A borrowed slice of the path, or a suffix of a shared reference-counted buffer.
49///
50/// The `Shared` variant is what makes owned paths cheap: cloning bumps a refcount and
51/// suffix operations (strip_prefix, next_part) only advance `start`, so one allocation
52/// serves every copy of a path as it fans out to consumers.
53#[derive(Clone)]
54enum Repr<'a> {
55	Borrowed(&'a str),
56	Shared { buf: Arc<str>, start: usize },
57}
58
59/// A broadcast path that provides safe prefix matching operations.
60///
61/// This type wraps a string but provides path-aware operations that respect
62/// delimiter boundaries, preventing issues like "foo" matching "foobar".
63///
64/// Paths are automatically trimmed of leading and trailing slashes on creation,
65/// making all slashes implicit at boundaries.
66/// All paths are RELATIVE; you cannot join with a leading slash to make an absolute path.
67///
68/// Owned paths ([`PathOwned`]) share one reference-counted allocation: cloning, converting
69/// a shared path with [`Path::to_owned`], and suffix operations like [`Path::strip_prefix`]
70/// do not copy the underlying bytes.
71///
72/// # Examples
73/// ```
74/// use moq_net::{Path};
75///
76/// // Creation automatically trims slashes
77/// let path1 = Path::new("/foo/bar/");
78/// let path2 = Path::new("foo/bar");
79/// assert_eq!(path1, path2);
80///
81/// // Methods accept both &str and Path
82/// let base = Path::new("api/v1");
83/// assert!(base.has_prefix("api"));
84/// assert!(base.has_prefix(&Path::new("api/v1")));
85///
86/// let joined = base.join("users");
87/// assert_eq!(joined.as_str(), "api/v1/users");
88/// ```
89#[derive(Clone)]
90pub struct Path<'a>(Repr<'a>);
91
92impl<'a> Path<'a> {
93	/// Maximum number of slash-separated parts in a path.
94	///
95	/// Matches the IETF moq-transport limit of 32 fields in a namespace tuple.
96	/// moq-lite enforces the same bound: encoding or decoding a deeper path fails,
97	/// and publishing one to an origin is rejected.
98	pub const MAX_PARTS: usize = 32;
99
100	/// Create a new Path from a string slice.
101	///
102	/// Leading and trailing slashes are automatically trimmed.
103	/// Multiple consecutive internal slashes are collapsed to single slashes.
104	pub fn new(s: &'a str) -> Self {
105		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
106
107		// Check if we need to normalize (has multiple consecutive slashes)
108		if trimmed.contains("//") {
109			// Only allocate if we actually need to normalize
110			let normalized = trimmed
111				.split('/')
112				.filter(|s| !s.is_empty())
113				.collect::<Vec<_>>()
114				.join("/");
115			Self(Repr::Shared {
116				buf: normalized.into(),
117				start: 0,
118			})
119		} else {
120			// No normalization needed - use borrowed string
121			Self(Repr::Borrowed(trimmed))
122		}
123	}
124
125	// A copy of this path skipping the first `n` bytes, reusing the shared buffer when possible.
126	fn slice_from(&'a self, n: usize) -> Path<'a> {
127		match &self.0 {
128			Repr::Borrowed(s) => Path(Repr::Borrowed(&s[n..])),
129			Repr::Shared { buf, start } => Path(Repr::Shared {
130				buf: buf.clone(),
131				start: start + n,
132			}),
133		}
134	}
135
136	/// Check if this path has the given prefix, respecting path boundaries.
137	///
138	/// Unlike String::starts_with, this ensures that "foo" does not match "foobar".
139	/// The prefix must either:
140	/// - Be exactly equal to this path
141	/// - Be followed by a '/' delimiter in the original path
142	/// - Be empty (matches everything)
143	///
144	/// # Examples
145	/// ```
146	/// use moq_net::Path;
147	///
148	/// let path = Path::new("foo/bar");
149	/// assert!(path.has_prefix("foo"));
150	/// assert!(path.has_prefix(&Path::new("foo")));
151	/// assert!(path.has_prefix("foo/"));
152	/// assert!(!path.has_prefix("fo"));
153	///
154	/// let path = Path::new("foobar");
155	/// assert!(!path.has_prefix("foo"));
156	/// ```
157	pub fn has_prefix(&self, prefix: impl AsPath) -> bool {
158		let prefix = prefix.as_path();
159
160		if prefix.is_empty() {
161			return true;
162		}
163
164		let s = self.as_str();
165		if !s.starts_with(prefix.as_str()) {
166			return false;
167		}
168
169		// Check if the prefix is the exact match
170		if s.len() == prefix.len() {
171			return true;
172		}
173
174		// Otherwise, ensure the character after the prefix is a delimiter
175		s.as_bytes().get(prefix.len()) == Some(&b'/')
176	}
177
178	pub fn strip_prefix(&'a self, prefix: impl AsPath) -> Option<Path<'a>> {
179		let prefix = prefix.as_path();
180
181		if prefix.is_empty() {
182			return Some(self.borrow());
183		}
184
185		let s = self.as_str();
186		if !s.starts_with(prefix.as_str()) {
187			return None;
188		}
189
190		// Check if the prefix is the exact match
191		if s.len() == prefix.len() {
192			return Some(Path::empty());
193		}
194
195		// Otherwise, ensure the character after the prefix is a delimiter
196		if s.as_bytes().get(prefix.len()) != Some(&b'/') {
197			return None;
198		}
199
200		Some(self.slice_from(prefix.len() + 1))
201	}
202
203	/// Iterate over the slash-separated parts of the path.
204	///
205	/// The empty path has no parts.
206	///
207	/// # Examples
208	/// ```
209	/// use moq_net::Path;
210	///
211	/// let path = Path::new("foo/bar/baz");
212	/// assert_eq!(path.parts().collect::<Vec<_>>(), ["foo", "bar", "baz"]);
213	/// assert_eq!(Path::empty().parts().count(), 0);
214	/// ```
215	pub fn parts(&self) -> impl Iterator<Item = &str> {
216		// Paths are normalized on creation so there are no empty parts to filter,
217		// except that splitting the empty path yields one empty item.
218		self.as_str().split('/').filter(|part| !part.is_empty())
219	}
220
221	/// Strip the directory component of the path, if any, and return the rest of the path.
222	pub fn next_part(&'a self) -> Option<(&'a str, Path<'a>)> {
223		let s = self.as_str();
224		if s.is_empty() {
225			return None;
226		}
227
228		if let Some(i) = s.find('/') {
229			Some((&s[..i], self.slice_from(i + 1)))
230		} else {
231			Some((s, Path::empty()))
232		}
233	}
234
235	pub fn as_str(&self) -> &str {
236		match &self.0 {
237			Repr::Borrowed(s) => s,
238			Repr::Shared { buf, start } => &buf[*start..],
239		}
240	}
241
242	pub fn empty() -> Path<'static> {
243		Path(Repr::Borrowed(""))
244	}
245
246	pub fn is_empty(&self) -> bool {
247		self.as_str().is_empty()
248	}
249
250	pub fn len(&self) -> usize {
251		self.as_str().len()
252	}
253
254	pub fn to_owned(&self) -> PathOwned {
255		match &self.0 {
256			Repr::Borrowed("") => Path::empty(),
257			Repr::Borrowed(s) => Path(Repr::Shared {
258				buf: Arc::from(*s),
259				start: 0,
260			}),
261			Repr::Shared { buf, start } => Path(Repr::Shared {
262				buf: buf.clone(),
263				start: *start,
264			}),
265		}
266	}
267
268	pub fn into_owned(self) -> PathOwned {
269		match self.0 {
270			Repr::Borrowed("") => Path::empty(),
271			Repr::Borrowed(s) => Path(Repr::Shared {
272				buf: Arc::from(s),
273				start: 0,
274			}),
275			Repr::Shared { buf, start } => Path(Repr::Shared { buf, start }),
276		}
277	}
278
279	/// A copy of this path bound to `self`'s lifetime, without copying the underlying bytes.
280	pub fn borrow(&'a self) -> Path<'a> {
281		self.slice_from(0)
282	}
283
284	/// Join this path with another path component.
285	///
286	/// # Examples
287	/// ```
288	/// use moq_net::Path;
289	///
290	/// let base = Path::new("foo");
291	/// let joined = base.join("bar");
292	/// assert_eq!(joined.as_str(), "foo/bar");
293	///
294	/// let joined = base.join(&Path::new("bar"));
295	/// assert_eq!(joined.as_str(), "foo/bar");
296	/// ```
297	pub fn join(&self, other: impl AsPath) -> PathOwned {
298		let other = other.as_path();
299
300		if self.is_empty() {
301			other.to_owned()
302		} else if other.is_empty() {
303			self.to_owned()
304		} else {
305			// Since paths are trimmed, we always need to add a slash
306			Path(Repr::Shared {
307				buf: format!("{}/{}", self.as_str(), other.as_str()).into(),
308				start: 0,
309			})
310		}
311	}
312}
313
314// Comparisons, ordering, and hashing all go through `as_str()` so a borrowed and a
315// shared path with the same content behave identically (e.g. as map keys).
316impl<'b> PartialEq<Path<'b>> for Path<'_> {
317	fn eq(&self, other: &Path<'b>) -> bool {
318		self.as_str() == other.as_str()
319	}
320}
321
322impl Eq for Path<'_> {}
323
324impl PartialOrd for Path<'_> {
325	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
326		Some(self.cmp(other))
327	}
328}
329
330impl Ord for Path<'_> {
331	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
332		self.as_str().cmp(other.as_str())
333	}
334}
335
336impl std::hash::Hash for Path<'_> {
337	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
338		self.as_str().hash(state)
339	}
340}
341
342impl fmt::Debug for Path<'_> {
343	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344		f.debug_tuple("Path").field(&self.as_str()).finish()
345	}
346}
347
348#[cfg(feature = "serde")]
349impl serde::Serialize for Path<'_> {
350	fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
351		serializer.serialize_str(self.as_str())
352	}
353}
354
355impl<'a> From<&'a str> for Path<'a> {
356	fn from(s: &'a str) -> Self {
357		Self::new(s)
358	}
359}
360
361impl<'a> From<&'a String> for Path<'a> {
362	fn from(s: &'a String) -> Self {
363		// TODO avoid making a copy here
364		Self::new(s)
365	}
366}
367
368impl Default for Path<'_> {
369	fn default() -> Self {
370		Path::empty()
371	}
372}
373
374impl From<String> for Path<'_> {
375	fn from(s: String) -> Self {
376		Path::new(&s).into_owned()
377	}
378}
379
380impl AsRef<str> for Path<'_> {
381	fn as_ref(&self) -> &str {
382		self.as_str()
383	}
384}
385
386impl Display for Path<'_> {
387	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388		write!(f, "{}", self.as_str())
389	}
390}
391
392impl<V: Copy> Decode<V> for Path<'_>
393where
394	String: Decode<V>,
395{
396	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
397		let path: Path = String::decode(r, version)?.into();
398		if path.parts().count() > Path::MAX_PARTS {
399			return Err(DecodeError::BoundsExceeded);
400		}
401		Ok(path)
402	}
403}
404
405impl<V: Copy> Encode<V> for Path<'_>
406where
407	for<'a> &'a str: Encode<V>,
408{
409	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
410		if self.parts().count() > Path::MAX_PARTS {
411			return Err(EncodeError::BoundsExceeded);
412		}
413		self.as_str().encode(w, version)?;
414		Ok(())
415	}
416}
417
418// A custom deserializer is needed in order to sanitize
419#[cfg(feature = "serde")]
420impl<'de: 'a, 'a> serde::Deserialize<'de> for Path<'a> {
421	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
422	where
423		D: serde::Deserializer<'de>,
424	{
425		let s = <&'a str as serde::Deserialize<'de>>::deserialize(deserializer)?;
426		Ok(Path::new(s))
427	}
428}
429
430/// A deduplicated list of path prefixes.
431///
432/// Automatically removes exact duplicates and overlapping prefixes on construction.
433/// For example, `["demo", "demo/foo", "anon"]` becomes `["demo", "anon"]` since
434/// `"demo"` already covers `"demo/foo"`.
435#[derive(Debug, Clone, Default, Eq)]
436pub struct PathPrefixes {
437	paths: Vec<PathOwned>,
438}
439
440impl PathPrefixes {
441	/// Create a new PathPrefixes, deduplicating and removing overlapping prefixes.
442	///
443	/// Shorter prefixes subsume longer ones: `"demo"` covers `"demo/foo"`.
444	///
445	/// Accepts anything iterable over path-like items:
446	/// ```
447	/// use moq_net::PathPrefixes;
448	///
449	/// let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
450	/// assert_eq!(list.len(), 2); // "demo/foo" subsumed by "demo"
451	/// ```
452	pub fn new(paths: impl IntoIterator<Item = impl AsPath>) -> Self {
453		let mut paths: Vec<PathOwned> = paths.into_iter().map(|p| p.as_path().to_owned()).collect();
454
455		if paths.len() <= 1 {
456			return Self { paths };
457		}
458
459		// Sort by length so shorter (more permissive) prefixes come first.
460		// Tie-break lexicographically for canonical ordering.
461		paths.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.as_str().cmp(b.as_str())));
462		paths.dedup();
463
464		let mut result: Vec<PathOwned> = Vec::new();
465		'outer: for path in paths {
466			for existing in &result {
467				if path.has_prefix(existing) {
468					continue 'outer;
469				}
470			}
471			result.push(path);
472		}
473
474		Self { paths: result }
475	}
476
477	pub fn is_empty(&self) -> bool {
478		self.paths.is_empty()
479	}
480
481	pub fn len(&self) -> usize {
482		self.paths.len()
483	}
484
485	pub fn iter(&self) -> std::slice::Iter<'_, PathOwned> {
486		self.paths.iter()
487	}
488}
489
490impl std::ops::Deref for PathPrefixes {
491	type Target = [PathOwned];
492
493	fn deref(&self) -> &[PathOwned] {
494		&self.paths
495	}
496}
497
498impl FromIterator<PathOwned> for PathPrefixes {
499	fn from_iter<I: IntoIterator<Item = PathOwned>>(iter: I) -> Self {
500		Self::new(iter)
501	}
502}
503
504impl From<Vec<PathOwned>> for PathPrefixes {
505	fn from(paths: Vec<PathOwned>) -> Self {
506		Self::new(paths)
507	}
508}
509
510impl<'a> PartialEq<Vec<Path<'a>>> for PathPrefixes {
511	fn eq(&self, other: &Vec<Path<'a>>) -> bool {
512		self.paths == *other
513	}
514}
515
516impl<'a> PartialEq<PathPrefixes> for Vec<Path<'a>> {
517	fn eq(&self, other: &PathPrefixes) -> bool {
518		*self == other.paths
519	}
520}
521
522impl PartialEq for PathPrefixes {
523	fn eq(&self, other: &Self) -> bool {
524		self.paths == other.paths
525	}
526}
527
528impl IntoIterator for PathPrefixes {
529	type Item = PathOwned;
530	type IntoIter = std::vec::IntoIter<PathOwned>;
531
532	fn into_iter(self) -> Self::IntoIter {
533		self.paths.into_iter()
534	}
535}
536
537impl<'a> IntoIterator for &'a PathPrefixes {
538	type Item = &'a PathOwned;
539	type IntoIter = std::slice::Iter<'a, PathOwned>;
540
541	fn into_iter(self) -> Self::IntoIter {
542		self.paths.iter()
543	}
544}
545
546#[cfg(test)]
547mod tests {
548	use super::*;
549
550	#[test]
551	fn test_has_prefix() {
552		let path = Path::new("foo/bar/baz");
553
554		// Valid prefixes - test with both &str and &Path
555		assert!(path.has_prefix(""));
556		assert!(path.has_prefix("foo"));
557		assert!(path.has_prefix(Path::new("foo")));
558		assert!(path.has_prefix("foo/"));
559		assert!(path.has_prefix("foo/bar"));
560		assert!(path.has_prefix(Path::new("foo/bar/")));
561		assert!(path.has_prefix("foo/bar/baz"));
562
563		// Invalid prefixes - should not match partial components
564		assert!(!path.has_prefix("f"));
565		assert!(!path.has_prefix(Path::new("fo")));
566		assert!(!path.has_prefix("foo/b"));
567		assert!(!path.has_prefix("foo/ba"));
568		assert!(!path.has_prefix(Path::new("foo/bar/ba")));
569
570		// Edge case: "foobar" should not match "foo"
571		let path = Path::new("foobar");
572		assert!(!path.has_prefix("foo"));
573		assert!(path.has_prefix(Path::new("foobar")));
574	}
575
576	#[test]
577	fn test_strip_prefix() {
578		let path = Path::new("foo/bar/baz");
579
580		// Test with both &str and &Path
581		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
582		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
583		assert_eq!(path.strip_prefix(Path::new("foo/")).unwrap().as_str(), "bar/baz");
584		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
585		assert_eq!(path.strip_prefix(Path::new("foo/bar/")).unwrap().as_str(), "baz");
586		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
587
588		// Should fail for invalid prefixes
589		assert!(path.strip_prefix("fo").is_none());
590		assert!(path.strip_prefix(Path::new("bar")).is_none());
591	}
592
593	#[test]
594	fn test_join() {
595		// Test with both &str and &Path
596		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
597		assert_eq!(Path::new("foo/").join(Path::new("bar")).as_str(), "foo/bar");
598		assert_eq!(Path::new("").join("bar").as_str(), "bar");
599		assert_eq!(Path::new("foo/bar").join(Path::new("baz")).as_str(), "foo/bar/baz");
600	}
601
602	#[test]
603	fn test_empty() {
604		let empty = Path::new("");
605		assert!(empty.is_empty());
606		assert_eq!(empty.len(), 0);
607
608		let non_empty = Path::new("foo");
609		assert!(!non_empty.is_empty());
610		assert_eq!(non_empty.len(), 3);
611	}
612
613	#[test]
614	fn test_from_conversions() {
615		let path1 = Path::from("foo/bar");
616		let path2 = Path::from("foo/bar");
617		let s = String::from("foo/bar");
618		let path3 = Path::from(&s);
619
620		assert_eq!(path1.as_str(), "foo/bar");
621		assert_eq!(path2.as_str(), "foo/bar");
622		assert_eq!(path3.as_str(), "foo/bar");
623	}
624
625	#[test]
626	fn test_path_prefix_join() {
627		let prefix = Path::new("foo");
628		let suffix = Path::new("bar/baz");
629		let path = prefix.join(&suffix);
630		assert_eq!(path.as_str(), "foo/bar/baz");
631
632		let prefix = Path::new("foo/");
633		let suffix = Path::new("bar/baz");
634		let path = prefix.join(&suffix);
635		assert_eq!(path.as_str(), "foo/bar/baz");
636
637		let prefix = Path::new("foo");
638		let suffix = Path::new("/bar/baz");
639		let path = prefix.join(&suffix);
640		assert_eq!(path.as_str(), "foo/bar/baz");
641
642		let prefix = Path::new("");
643		let suffix = Path::new("bar/baz");
644		let path = prefix.join(&suffix);
645		assert_eq!(path.as_str(), "bar/baz");
646	}
647
648	#[test]
649	fn test_path_prefix_conversions() {
650		let prefix1 = Path::from("foo/bar");
651		let prefix2 = Path::from(String::from("foo/bar"));
652		let s = String::from("foo/bar");
653		let prefix3 = Path::from(&s);
654
655		assert_eq!(prefix1.as_str(), "foo/bar");
656		assert_eq!(prefix2.as_str(), "foo/bar");
657		assert_eq!(prefix3.as_str(), "foo/bar");
658	}
659
660	#[test]
661	fn test_path_suffix_conversions() {
662		let suffix1 = Path::from("foo/bar");
663		let suffix2 = Path::from(String::from("foo/bar"));
664		let s = String::from("foo/bar");
665		let suffix3 = Path::from(&s);
666
667		assert_eq!(suffix1.as_str(), "foo/bar");
668		assert_eq!(suffix2.as_str(), "foo/bar");
669		assert_eq!(suffix3.as_str(), "foo/bar");
670	}
671
672	#[test]
673	fn test_path_types_basic_operations() {
674		let prefix = Path::new("foo/bar");
675		assert_eq!(prefix.as_str(), "foo/bar");
676		assert!(!prefix.is_empty());
677		assert_eq!(prefix.len(), 7);
678
679		let suffix = Path::new("baz/qux");
680		assert_eq!(suffix.as_str(), "baz/qux");
681		assert!(!suffix.is_empty());
682		assert_eq!(suffix.len(), 7);
683
684		let empty_prefix = Path::new("");
685		assert!(empty_prefix.is_empty());
686		assert_eq!(empty_prefix.len(), 0);
687
688		let empty_suffix = Path::new("");
689		assert!(empty_suffix.is_empty());
690		assert_eq!(empty_suffix.len(), 0);
691	}
692
693	#[test]
694	fn test_prefix_has_prefix() {
695		// Test empty prefix (should match everything)
696		let prefix = Path::new("foo/bar");
697		assert!(prefix.has_prefix(""));
698
699		// Test exact matches
700		let prefix = Path::new("foo/bar");
701		assert!(prefix.has_prefix("foo/bar"));
702
703		// Test valid prefixes
704		assert!(prefix.has_prefix("foo"));
705		assert!(prefix.has_prefix("foo/"));
706
707		// Test invalid prefixes - partial matches should fail
708		assert!(!prefix.has_prefix("f"));
709		assert!(!prefix.has_prefix("fo"));
710		assert!(!prefix.has_prefix("foo/b"));
711		assert!(!prefix.has_prefix("foo/ba"));
712
713		// Test edge cases
714		let prefix = Path::new("foobar");
715		assert!(!prefix.has_prefix("foo"));
716		assert!(prefix.has_prefix("foobar"));
717
718		// Test trailing slash handling
719		let prefix = Path::new("foo/bar/");
720		assert!(prefix.has_prefix("foo"));
721		assert!(prefix.has_prefix("foo/"));
722		assert!(prefix.has_prefix("foo/bar"));
723		assert!(prefix.has_prefix("foo/bar/"));
724
725		// Test single component
726		let prefix = Path::new("foo");
727		assert!(prefix.has_prefix(""));
728		assert!(prefix.has_prefix("foo"));
729		assert!(prefix.has_prefix("foo/")); // "foo/" becomes "foo" after trimming
730		assert!(!prefix.has_prefix("f"));
731
732		// Test empty prefix
733		let prefix = Path::new("");
734		assert!(prefix.has_prefix(""));
735		assert!(!prefix.has_prefix("foo"));
736	}
737
738	#[test]
739	fn test_prefix_join() {
740		// Basic joining
741		let prefix = Path::new("foo");
742		let suffix = Path::new("bar");
743		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
744
745		// Trailing slash on prefix
746		let prefix = Path::new("foo/");
747		let suffix = Path::new("bar");
748		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
749
750		// Leading slash on suffix
751		let prefix = Path::new("foo");
752		let suffix = Path::new("/bar");
753		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
754
755		// Trailing slash on suffix
756		let prefix = Path::new("foo");
757		let suffix = Path::new("bar/");
758		assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); // trailing slash is trimmed
759
760		// Both have slashes
761		let prefix = Path::new("foo/");
762		let suffix = Path::new("/bar");
763		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
764
765		// Empty suffix
766		let prefix = Path::new("foo");
767		let suffix = Path::new("");
768		assert_eq!(prefix.join(suffix).as_str(), "foo");
769
770		// Empty prefix
771		let prefix = Path::new("");
772		let suffix = Path::new("bar");
773		assert_eq!(prefix.join(suffix).as_str(), "bar");
774
775		// Both empty
776		let prefix = Path::new("");
777		let suffix = Path::new("");
778		assert_eq!(prefix.join(suffix).as_str(), "");
779
780		// Complex paths
781		let prefix = Path::new("foo/bar");
782		let suffix = Path::new("baz/qux");
783		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux");
784
785		// Complex paths with slashes
786		let prefix = Path::new("foo/bar/");
787		let suffix = Path::new("/baz/qux/");
788		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
789	}
790
791	#[test]
792	fn test_path_ref() {
793		// Test PathRef creation and normalization
794		let ref1 = Path::new("/foo/bar/");
795		assert_eq!(ref1.as_str(), "foo/bar");
796
797		let ref2 = Path::from("///foo///");
798		assert_eq!(ref2.as_str(), "foo");
799
800		// Test PathRef normalizes multiple slashes
801		let ref3 = Path::new("foo//bar///baz");
802		assert_eq!(ref3.as_str(), "foo/bar/baz");
803
804		// Test conversions
805		let path = Path::new("foo/bar");
806		let path_ref = path;
807		assert_eq!(path_ref.as_str(), "foo/bar");
808
809		// Test that Path methods work with PathRef
810		let path2 = Path::new("foo/bar/baz");
811		assert!(path2.has_prefix(&path_ref));
812		assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz");
813
814		// Test empty PathRef
815		let empty = Path::new("");
816		assert!(empty.is_empty());
817		assert_eq!(empty.len(), 0);
818	}
819
820	#[test]
821	fn test_multiple_consecutive_slashes() {
822		let path = Path::new("foo//bar///baz");
823		// Multiple consecutive slashes are collapsed to single slashes
824		assert_eq!(path.as_str(), "foo/bar/baz");
825
826		// Test with leading and trailing slashes too
827		let path2 = Path::new("//foo//bar///baz//");
828		assert_eq!(path2.as_str(), "foo/bar/baz");
829
830		// Test empty segments are handled correctly
831		let path3 = Path::new("foo///bar");
832		assert_eq!(path3.as_str(), "foo/bar");
833	}
834
835	#[test]
836	fn test_removes_multiple_slashes_comprehensively() {
837		// Test various multiple slash scenarios
838		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
839		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
840		assert_eq!(Path::new("foo////bar").as_str(), "foo/bar");
841
842		// Multiple occurrences of double slashes
843		assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz");
844		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
845
846		// Mixed slash counts
847		assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux");
848
849		// With leading and trailing slashes
850		assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar");
851		assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar");
852
853		// Edge case: only slashes
854		assert_eq!(Path::new("//").as_str(), "");
855		assert_eq!(Path::new("////").as_str(), "");
856
857		// Test that operations work correctly with normalized paths
858		let path_with_slashes = Path::new("foo//bar///baz");
859		assert!(path_with_slashes.has_prefix("foo/bar"));
860		assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz");
861		assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux");
862
863		// Test PathRef to Path conversion
864		let path_ref = Path::new("foo//bar///baz");
865		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too
866		let path_from_ref = path_ref.to_owned();
867		assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized
868	}
869
870	#[test]
871	fn test_path_ref_multiple_slashes() {
872		// PathRef now normalizes multiple slashes using Cow
873		let path_ref = Path::new("//foo//bar///baz//");
874		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized
875
876		// Various multiple slash scenarios are normalized in PathRef
877		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
878		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
879		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
880
881		// Conversion to Path maintains normalized form
882		assert_eq!(Path::new("foo//bar").to_owned().as_str(), "foo/bar");
883		assert_eq!(Path::new("foo///bar").to_owned().as_str(), "foo/bar");
884		assert_eq!(Path::new("a//b//c//d").to_owned().as_str(), "a/b/c/d");
885
886		// Edge cases
887		assert_eq!(Path::new("//").as_str(), "");
888		assert_eq!(Path::new("////").as_str(), "");
889		assert_eq!(Path::new("//").to_owned().as_str(), "");
890		assert_eq!(Path::new("////").to_owned().as_str(), "");
891
892		// Test that PathRef avoids allocation when no normalization needed
893		let normal_path = Path::new("foo/bar/baz");
894		assert_eq!(normal_path.as_str(), "foo/bar/baz");
895		// This should use Cow::Borrowed internally (no allocation)
896
897		let needs_norm = Path::new("foo//bar");
898		assert_eq!(needs_norm.as_str(), "foo/bar");
899		// This should use Cow::Owned internally (allocation only when needed)
900	}
901
902	#[test]
903	fn test_ergonomic_conversions() {
904		// Test that all these work ergonomically in function calls
905		fn takes_path_ref<'a>(p: impl Into<Path<'a>>) -> String {
906			p.into().as_str().to_string()
907		}
908
909		// Alternative API using the trait alias for better error messages
910		fn takes_path_ref_with_trait<'a>(p: impl Into<Path<'a>>) -> String {
911			p.into().as_str().to_string()
912		}
913
914		// String literal
915		assert_eq!(takes_path_ref("foo//bar"), "foo/bar");
916
917		// String (owned) - this should now work without &
918		let owned_string = String::from("foo//bar///baz");
919		assert_eq!(takes_path_ref(owned_string), "foo/bar/baz");
920
921		// &String
922		let string_ref = String::from("foo//bar");
923		assert_eq!(takes_path_ref(string_ref), "foo/bar");
924
925		// PathRef
926		let path_ref = Path::new("foo//bar");
927		assert_eq!(takes_path_ref(path_ref), "foo/bar");
928
929		// Path
930		let path = Path::new("foo//bar");
931		assert_eq!(takes_path_ref(path), "foo/bar");
932
933		// Test that Path::new works with all these types
934		let _path1 = Path::new("foo/bar"); // &str
935		let _path2 = Path::new("foo/bar"); // String - should now work
936		let _path3 = Path::new("foo/bar"); // &String
937		let _path4 = Path::new("foo/bar"); // PathRef
938
939		// Test the trait alias version works the same
940		assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar");
941		assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar");
942	}
943
944	#[test]
945	fn test_prefix_strip_prefix() {
946		// Test basic stripping
947		let prefix = Path::new("foo/bar/baz");
948		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
949		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar/baz");
950		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar/baz");
951		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "baz");
952		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "baz");
953		assert_eq!(prefix.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
954
955		// Test invalid prefixes
956		assert!(prefix.strip_prefix("fo").is_none());
957		assert!(prefix.strip_prefix("bar").is_none());
958		assert!(prefix.strip_prefix("foo/ba").is_none());
959
960		// Test edge cases
961		let prefix = Path::new("foobar");
962		assert!(prefix.strip_prefix("foo").is_none());
963		assert_eq!(prefix.strip_prefix("foobar").unwrap().as_str(), "");
964
965		// Test empty prefix
966		let prefix = Path::new("");
967		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "");
968		assert!(prefix.strip_prefix("foo").is_none());
969
970		// Test single component
971		let prefix = Path::new("foo");
972		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "");
973		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
974
975		// Test trailing slash handling
976		let prefix = Path::new("foo/bar/");
977		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar");
978		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar");
979		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "");
980		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "");
981	}
982
983	#[test]
984	fn test_prefix_list_dedup() {
985		// Exact duplicates are removed
986		let list = PathPrefixes::new(["demo", "demo"]);
987		assert_eq!(list.len(), 1);
988		assert_eq!(list[0], Path::new("demo"));
989	}
990
991	#[test]
992	fn test_prefix_list_overlap() {
993		// "demo/foo" is redundant when "demo" exists
994		let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
995		assert_eq!(list.len(), 2);
996		assert!(list.iter().any(|p| p == &Path::new("demo")));
997		assert!(list.iter().any(|p| p == &Path::new("anon")));
998	}
999
1000	#[test]
1001	fn test_prefix_list_overlap_reverse_order() {
1002		// Order shouldn't matter
1003		let list = PathPrefixes::new(["demo/foo", "demo"]);
1004		assert_eq!(list.len(), 1);
1005		assert_eq!(list[0], Path::new("demo"));
1006	}
1007
1008	#[test]
1009	fn test_prefix_list_empty_covers_all() {
1010		// Empty prefix covers everything
1011		let list = PathPrefixes::new(["", "demo", "anon"]);
1012		assert_eq!(list.len(), 1);
1013		assert_eq!(list[0], Path::new(""));
1014	}
1015
1016	#[test]
1017	fn test_prefix_list_no_overlap() {
1018		// Unrelated prefixes are all kept
1019		let list = PathPrefixes::new(["demo", "anon", "secret"]);
1020		assert_eq!(list.len(), 3);
1021	}
1022
1023	#[test]
1024	fn test_prefix_list_single() {
1025		let list = PathPrefixes::new(["demo"]);
1026		assert_eq!(list.len(), 1);
1027	}
1028
1029	#[test]
1030	fn test_prefix_list_empty() {
1031		let list = PathPrefixes::new(std::iter::empty::<&str>());
1032		assert!(list.is_empty());
1033		assert_eq!(list.len(), 0);
1034	}
1035
1036	#[test]
1037	fn test_prefix_list_deep_overlap() {
1038		// "a/b/c" is covered by "a/b" which is covered by "a"
1039		let list = PathPrefixes::new(["a/b/c", "a/b", "a"]);
1040		assert_eq!(list.len(), 1);
1041		assert_eq!(list[0], Path::new("a"));
1042	}
1043
1044	#[test]
1045	fn test_prefix_list_partial_name_not_overlap() {
1046		// "demo" should NOT cover "demonstration" (different path component)
1047		let list = PathPrefixes::new(["demo", "demonstration"]);
1048		assert_eq!(list.len(), 2);
1049	}
1050
1051	#[test]
1052	fn test_prefix_list_collect() {
1053		let paths: Vec<PathOwned> = vec!["demo".into(), "demo/foo".into()];
1054		let list: PathPrefixes = paths.into_iter().collect();
1055		assert_eq!(list.len(), 1);
1056		assert_eq!(list[0], Path::new("demo"));
1057	}
1058
1059	#[test]
1060	fn test_prefix_list_eq_vec() {
1061		let list = PathPrefixes::new(["demo", "anon"]);
1062		// Canonical order: sorted by length, then lexicographically
1063		assert_eq!(list, vec!["anon".as_path(), "demo".as_path()]);
1064	}
1065
1066	// Pointer-equality checks that owned paths share one allocation through the
1067	// clone / to_owned / strip_prefix flow used by origin announce fan-out.
1068	#[test]
1069	fn test_owned_paths_share_allocation() {
1070		let path = Path::new("customer/room/broadcast").to_owned();
1071
1072		// Cloning an owned path shares the buffer.
1073		let cloned = path.clone();
1074		assert_eq!(path.as_str().as_ptr(), cloned.as_str().as_ptr());
1075
1076		// as_path + to_owned (how notify queues a path per consumer) shares too.
1077		let requeued = path.as_path().to_owned();
1078		assert_eq!(path.as_str().as_ptr(), requeued.as_str().as_ptr());
1079
1080		// Stripping a prefix from an owned path is offset arithmetic, not a copy.
1081		let stripped = path.strip_prefix("customer").unwrap().to_owned();
1082		assert_eq!(stripped.as_str(), "room/broadcast");
1083		assert_eq!(stripped.as_str().as_ptr(), path.as_str()["customer/".len()..].as_ptr());
1084
1085		// next_part shares the rest as well.
1086		let (dir, rest) = path.next_part().unwrap();
1087		assert_eq!(dir, "customer");
1088		let rest = rest.to_owned();
1089		assert_eq!(rest.as_str().as_ptr(), stripped.as_str().as_ptr());
1090
1091		// join produces an owned path whose clones share.
1092		let joined = path.join("alice");
1093		let joined2 = joined.clone();
1094		assert_eq!(joined.as_str(), "customer/room/broadcast/alice");
1095		assert_eq!(joined.as_str().as_ptr(), joined2.as_str().as_ptr());
1096	}
1097
1098	#[test]
1099	fn test_parts() {
1100		assert_eq!(Path::empty().parts().count(), 0);
1101		assert_eq!(Path::new("foo").parts().collect::<Vec<_>>(), ["foo"]);
1102		assert_eq!(Path::new("/foo//bar/").parts().collect::<Vec<_>>(), ["foo", "bar"]);
1103	}
1104
1105	#[test]
1106	fn test_wire_max_parts() {
1107		use crate::lite::Version;
1108
1109		let ok = (0..Path::MAX_PARTS)
1110			.map(|i| i.to_string())
1111			.collect::<Vec<_>>()
1112			.join("/");
1113		let too_deep = format!("{ok}/extra");
1114
1115		// Encode enforces the limit.
1116		let mut buf = bytes::BytesMut::new();
1117		Path::new(&ok).encode(&mut buf, Version::Lite04).unwrap();
1118		assert!(matches!(
1119			Path::new(&too_deep).encode(&mut bytes::BytesMut::new(), Version::Lite04),
1120			Err(EncodeError::BoundsExceeded)
1121		));
1122
1123		// Decode round-trips at the limit.
1124		let decoded = Path::decode(&mut buf.freeze(), Version::Lite04).unwrap();
1125		assert_eq!(decoded.as_str(), ok);
1126
1127		// Decode enforces the limit on a raw string that encode would have refused.
1128		let mut buf = bytes::BytesMut::new();
1129		too_deep.as_str().encode(&mut buf, Version::Lite04).unwrap();
1130		assert!(matches!(
1131			Path::decode(&mut buf.freeze(), Version::Lite04),
1132			Err(DecodeError::BoundsExceeded)
1133		));
1134	}
1135
1136	#[test]
1137	fn test_owned_empty_paths() {
1138		// Empty paths never allocate and stay well-behaved.
1139		let empty = Path::new("").to_owned();
1140		assert!(empty.is_empty());
1141		assert_eq!(empty, Path::empty());
1142
1143		let path = Path::new("foo").to_owned();
1144		let rest = path.strip_prefix("foo").unwrap().to_owned();
1145		assert!(rest.is_empty());
1146	}
1147
1148	#[test]
1149	fn test_prefix_list_canonical_order() {
1150		// Same inputs in different order produce identical results
1151		let a = PathPrefixes::new(["foo", "bar"]);
1152		let b = PathPrefixes::new(["bar", "foo"]);
1153		assert_eq!(a, b);
1154	}
1155}