Skip to main content

moq_lite/
path.rs

1use std::borrow::Cow;
2use std::fmt::{self, Display};
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 as a reference is returned.
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 make a copy.
26		Path(Cow::Borrowed(self.as_str()))
27	}
28}
29
30impl AsPath for Path<'_> {
31	fn as_path(&self) -> Path<'_> {
32		Path(Cow::Borrowed(self.0.as_ref()))
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 broadcast path that provides safe prefix matching operations.
49///
50/// This type wraps a String but provides path-aware operations that respect
51/// delimiter boundaries, preventing issues like "foo" matching "foobar".
52///
53/// Paths are automatically trimmed of leading and trailing slashes on creation,
54/// making all slashes implicit at boundaries.
55/// All paths are RELATIVE; you cannot join with a leading slash to make an absolute path.
56///
57/// # Examples
58/// ```
59/// use moq_lite::{Path};
60///
61/// // Creation automatically trims slashes
62/// let path1 = Path::new("/foo/bar/");
63/// let path2 = Path::new("foo/bar");
64/// assert_eq!(path1, path2);
65///
66/// // Methods accept both &str and Path
67/// let base = Path::new("api/v1");
68/// assert!(base.has_prefix("api"));
69/// assert!(base.has_prefix(&Path::new("api/v1")));
70///
71/// let joined = base.join("users");
72/// assert_eq!(joined.as_str(), "api/v1/users");
73/// ```
74#[derive(Debug, PartialEq, Eq, Hash, Clone)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize))]
76pub struct Path<'a>(Cow<'a, str>);
77
78impl<'a> Path<'a> {
79	/// Create a new Path from a string slice.
80	///
81	/// Leading and trailing slashes are automatically trimmed.
82	/// Multiple consecutive internal slashes are collapsed to single slashes.
83	pub fn new(s: &'a str) -> Self {
84		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
85
86		// Check if we need to normalize (has multiple consecutive slashes)
87		if trimmed.contains("//") {
88			// Only allocate if we actually need to normalize
89			let normalized = trimmed
90				.split('/')
91				.filter(|s| !s.is_empty())
92				.collect::<Vec<_>>()
93				.join("/");
94			Self(Cow::Owned(normalized))
95		} else {
96			// No normalization needed - use borrowed string
97			Self(Cow::Borrowed(trimmed))
98		}
99	}
100
101	/// Check if this path has the given prefix, respecting path boundaries.
102	///
103	/// Unlike String::starts_with, this ensures that "foo" does not match "foobar".
104	/// The prefix must either:
105	/// - Be exactly equal to this path
106	/// - Be followed by a '/' delimiter in the original path
107	/// - Be empty (matches everything)
108	///
109	/// # Examples
110	/// ```
111	/// use moq_lite::Path;
112	///
113	/// let path = Path::new("foo/bar");
114	/// assert!(path.has_prefix("foo"));
115	/// assert!(path.has_prefix(&Path::new("foo")));
116	/// assert!(path.has_prefix("foo/"));
117	/// assert!(!path.has_prefix("fo"));
118	///
119	/// let path = Path::new("foobar");
120	/// assert!(!path.has_prefix("foo"));
121	/// ```
122	pub fn has_prefix(&self, prefix: impl AsPath) -> bool {
123		let prefix = prefix.as_path();
124
125		if prefix.is_empty() {
126			return true;
127		}
128
129		if !self.0.starts_with(prefix.as_str()) {
130			return false;
131		}
132
133		// Check if the prefix is the exact match
134		if self.0.len() == prefix.len() {
135			return true;
136		}
137
138		// Otherwise, ensure the character after the prefix is a delimiter
139		self.0.as_bytes().get(prefix.len()) == Some(&b'/')
140	}
141
142	pub fn strip_prefix(&'a self, prefix: impl AsPath) -> Option<Path<'a>> {
143		let prefix = prefix.as_path();
144
145		if prefix.is_empty() {
146			return Some(self.borrow());
147		}
148
149		if !self.0.starts_with(prefix.as_str()) {
150			return None;
151		}
152
153		// Check if the prefix is the exact match
154		if self.0.len() == prefix.len() {
155			return Some(Path(Cow::Borrowed("")));
156		}
157
158		// Otherwise, ensure the character after the prefix is a delimiter
159		if self.0.as_bytes().get(prefix.len()) != Some(&b'/') {
160			return None;
161		}
162
163		Some(Path(Cow::Borrowed(&self.0[prefix.len() + 1..])))
164	}
165
166	/// Strip the directory component of the path, if any, and return the rest of the path.
167	pub fn next_part(&'a self) -> Option<(&'a str, Path<'a>)> {
168		if self.0.is_empty() {
169			return None;
170		}
171
172		if let Some(i) = self.0.find('/') {
173			let dir = &self.0[..i];
174			let rest = Path(Cow::Borrowed(&self.0[i + 1..]));
175			Some((dir, rest))
176		} else {
177			Some((&self.0, Path(Cow::Borrowed(""))))
178		}
179	}
180
181	pub fn as_str(&self) -> &str {
182		&self.0
183	}
184
185	pub fn empty() -> Path<'static> {
186		Path(Cow::Borrowed(""))
187	}
188
189	pub fn is_empty(&self) -> bool {
190		self.0.is_empty()
191	}
192
193	pub fn len(&self) -> usize {
194		self.0.len()
195	}
196
197	pub fn to_owned(&self) -> PathOwned {
198		Path(Cow::Owned(self.0.to_string()))
199	}
200
201	pub fn into_owned(self) -> PathOwned {
202		Path(Cow::Owned(self.0.to_string()))
203	}
204
205	pub fn borrow(&'a self) -> Path<'a> {
206		Path(Cow::Borrowed(&self.0))
207	}
208
209	/// Join this path with another path component.
210	///
211	/// # Examples
212	/// ```
213	/// use moq_lite::Path;
214	///
215	/// let base = Path::new("foo");
216	/// let joined = base.join("bar");
217	/// assert_eq!(joined.as_str(), "foo/bar");
218	///
219	/// let joined = base.join(&Path::new("bar"));
220	/// assert_eq!(joined.as_str(), "foo/bar");
221	/// ```
222	pub fn join(&self, other: impl AsPath) -> PathOwned {
223		let other = other.as_path();
224
225		if self.0.is_empty() {
226			Path(Cow::Owned(other.0.to_string()))
227		} else if other.is_empty() {
228			// Technically, we could avoid allocating here, but it's nicer to return a PathOwned.
229			self.to_owned()
230		} else {
231			// Since paths are trimmed, we always need to add a slash
232			Path(Cow::Owned(format!("{}/{}", self.0, other.as_str())))
233		}
234	}
235}
236
237impl<'a> From<&'a str> for Path<'a> {
238	fn from(s: &'a str) -> Self {
239		Self::new(s)
240	}
241}
242
243impl<'a> From<&'a String> for Path<'a> {
244	fn from(s: &'a String) -> Self {
245		// TODO avoid making a copy here
246		Self::new(s)
247	}
248}
249
250impl Default for Path<'_> {
251	fn default() -> Self {
252		Self(Cow::Borrowed(""))
253	}
254}
255
256impl From<String> for Path<'_> {
257	fn from(s: String) -> Self {
258		// It's annoying that this logic is duplicated, but I couldn't figure out how to reuse Path::new.
259		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
260
261		// Check if we need to normalize (has multiple consecutive slashes)
262		if trimmed.contains("//") {
263			// Only allocate if we actually need to normalize
264			let normalized = trimmed
265				.split('/')
266				.filter(|s| !s.is_empty())
267				.collect::<Vec<_>>()
268				.join("/");
269			Self(Cow::Owned(normalized))
270		} else if trimmed == s {
271			// String is already trimmed and normalized, use it directly
272			Self(Cow::Owned(s))
273		} else {
274			// Need to trim but don't need to normalize internal slashes
275			Self(Cow::Owned(trimmed.to_string()))
276		}
277	}
278}
279
280impl AsRef<str> for Path<'_> {
281	fn as_ref(&self) -> &str {
282		&self.0
283	}
284}
285
286impl Display for Path<'_> {
287	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288		write!(f, "{}", self.0)
289	}
290}
291
292impl<V: Copy> Decode<V> for Path<'_>
293where
294	String: Decode<V>,
295{
296	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
297		Ok(String::decode(r, version)?.into())
298	}
299}
300
301impl<V: Copy> Encode<V> for Path<'_>
302where
303	for<'a> &'a str: Encode<V>,
304{
305	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
306		self.as_str().encode(w, version)?;
307		Ok(())
308	}
309}
310
311// A custom deserializer is needed in order to sanitize
312#[cfg(feature = "serde")]
313impl<'de: 'a, 'a> serde::Deserialize<'de> for Path<'a> {
314	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
315	where
316		D: serde::Deserializer<'de>,
317	{
318		let s = <&'a str as serde::Deserialize<'de>>::deserialize(deserializer)?;
319		Ok(Path::new(s))
320	}
321}
322
323/// A deduplicated list of path prefixes.
324///
325/// Automatically removes exact duplicates and overlapping prefixes on construction.
326/// For example, `["demo", "demo/foo", "anon"]` becomes `["demo", "anon"]` since
327/// `"demo"` already covers `"demo/foo"`.
328#[derive(Debug, Clone, Default, Eq)]
329pub struct PathPrefixes {
330	paths: Vec<PathOwned>,
331}
332
333impl PathPrefixes {
334	/// Create a new PathPrefixes, deduplicating and removing overlapping prefixes.
335	///
336	/// Shorter prefixes subsume longer ones: `"demo"` covers `"demo/foo"`.
337	///
338	/// Accepts anything iterable over path-like items:
339	/// ```
340	/// use moq_lite::PathPrefixes;
341	///
342	/// let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
343	/// assert_eq!(list.len(), 2); // "demo/foo" subsumed by "demo"
344	/// ```
345	pub fn new(paths: impl IntoIterator<Item = impl AsPath>) -> Self {
346		let mut paths: Vec<PathOwned> = paths.into_iter().map(|p| p.as_path().to_owned()).collect();
347
348		if paths.len() <= 1 {
349			return Self { paths };
350		}
351
352		// Sort by length so shorter (more permissive) prefixes come first.
353		// Tie-break lexicographically for canonical ordering.
354		paths.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.as_str().cmp(b.as_str())));
355		paths.dedup();
356
357		let mut result: Vec<PathOwned> = Vec::new();
358		'outer: for path in paths {
359			for existing in &result {
360				if path.has_prefix(existing) {
361					continue 'outer;
362				}
363			}
364			result.push(path);
365		}
366
367		Self { paths: result }
368	}
369
370	pub fn is_empty(&self) -> bool {
371		self.paths.is_empty()
372	}
373
374	pub fn len(&self) -> usize {
375		self.paths.len()
376	}
377
378	pub fn iter(&self) -> std::slice::Iter<'_, PathOwned> {
379		self.paths.iter()
380	}
381}
382
383impl std::ops::Deref for PathPrefixes {
384	type Target = [PathOwned];
385
386	fn deref(&self) -> &[PathOwned] {
387		&self.paths
388	}
389}
390
391impl FromIterator<PathOwned> for PathPrefixes {
392	fn from_iter<I: IntoIterator<Item = PathOwned>>(iter: I) -> Self {
393		Self::new(iter)
394	}
395}
396
397impl From<Vec<PathOwned>> for PathPrefixes {
398	fn from(paths: Vec<PathOwned>) -> Self {
399		Self::new(paths)
400	}
401}
402
403impl<'a> PartialEq<Vec<Path<'a>>> for PathPrefixes {
404	fn eq(&self, other: &Vec<Path<'a>>) -> bool {
405		self.paths == *other
406	}
407}
408
409impl<'a> PartialEq<PathPrefixes> for Vec<Path<'a>> {
410	fn eq(&self, other: &PathPrefixes) -> bool {
411		*self == other.paths
412	}
413}
414
415impl PartialEq for PathPrefixes {
416	fn eq(&self, other: &Self) -> bool {
417		self.paths == other.paths
418	}
419}
420
421impl IntoIterator for PathPrefixes {
422	type Item = PathOwned;
423	type IntoIter = std::vec::IntoIter<PathOwned>;
424
425	fn into_iter(self) -> Self::IntoIter {
426		self.paths.into_iter()
427	}
428}
429
430impl<'a> IntoIterator for &'a PathPrefixes {
431	type Item = &'a PathOwned;
432	type IntoIter = std::slice::Iter<'a, PathOwned>;
433
434	fn into_iter(self) -> Self::IntoIter {
435		self.paths.iter()
436	}
437}
438
439#[cfg(test)]
440mod tests {
441	use super::*;
442
443	#[test]
444	fn test_has_prefix() {
445		let path = Path::new("foo/bar/baz");
446
447		// Valid prefixes - test with both &str and &Path
448		assert!(path.has_prefix(""));
449		assert!(path.has_prefix("foo"));
450		assert!(path.has_prefix(Path::new("foo")));
451		assert!(path.has_prefix("foo/"));
452		assert!(path.has_prefix("foo/bar"));
453		assert!(path.has_prefix(Path::new("foo/bar/")));
454		assert!(path.has_prefix("foo/bar/baz"));
455
456		// Invalid prefixes - should not match partial components
457		assert!(!path.has_prefix("f"));
458		assert!(!path.has_prefix(Path::new("fo")));
459		assert!(!path.has_prefix("foo/b"));
460		assert!(!path.has_prefix("foo/ba"));
461		assert!(!path.has_prefix(Path::new("foo/bar/ba")));
462
463		// Edge case: "foobar" should not match "foo"
464		let path = Path::new("foobar");
465		assert!(!path.has_prefix("foo"));
466		assert!(path.has_prefix(Path::new("foobar")));
467	}
468
469	#[test]
470	fn test_strip_prefix() {
471		let path = Path::new("foo/bar/baz");
472
473		// Test with both &str and &Path
474		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
475		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
476		assert_eq!(path.strip_prefix(Path::new("foo/")).unwrap().as_str(), "bar/baz");
477		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
478		assert_eq!(path.strip_prefix(Path::new("foo/bar/")).unwrap().as_str(), "baz");
479		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
480
481		// Should fail for invalid prefixes
482		assert!(path.strip_prefix("fo").is_none());
483		assert!(path.strip_prefix(Path::new("bar")).is_none());
484	}
485
486	#[test]
487	fn test_join() {
488		// Test with both &str and &Path
489		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
490		assert_eq!(Path::new("foo/").join(Path::new("bar")).as_str(), "foo/bar");
491		assert_eq!(Path::new("").join("bar").as_str(), "bar");
492		assert_eq!(Path::new("foo/bar").join(Path::new("baz")).as_str(), "foo/bar/baz");
493	}
494
495	#[test]
496	fn test_empty() {
497		let empty = Path::new("");
498		assert!(empty.is_empty());
499		assert_eq!(empty.len(), 0);
500
501		let non_empty = Path::new("foo");
502		assert!(!non_empty.is_empty());
503		assert_eq!(non_empty.len(), 3);
504	}
505
506	#[test]
507	fn test_from_conversions() {
508		let path1 = Path::from("foo/bar");
509		let path2 = Path::from("foo/bar");
510		let s = String::from("foo/bar");
511		let path3 = Path::from(&s);
512
513		assert_eq!(path1.as_str(), "foo/bar");
514		assert_eq!(path2.as_str(), "foo/bar");
515		assert_eq!(path3.as_str(), "foo/bar");
516	}
517
518	#[test]
519	fn test_path_prefix_join() {
520		let prefix = Path::new("foo");
521		let suffix = Path::new("bar/baz");
522		let path = prefix.join(&suffix);
523		assert_eq!(path.as_str(), "foo/bar/baz");
524
525		let prefix = Path::new("foo/");
526		let suffix = Path::new("bar/baz");
527		let path = prefix.join(&suffix);
528		assert_eq!(path.as_str(), "foo/bar/baz");
529
530		let prefix = Path::new("foo");
531		let suffix = Path::new("/bar/baz");
532		let path = prefix.join(&suffix);
533		assert_eq!(path.as_str(), "foo/bar/baz");
534
535		let prefix = Path::new("");
536		let suffix = Path::new("bar/baz");
537		let path = prefix.join(&suffix);
538		assert_eq!(path.as_str(), "bar/baz");
539	}
540
541	#[test]
542	fn test_path_prefix_conversions() {
543		let prefix1 = Path::from("foo/bar");
544		let prefix2 = Path::from(String::from("foo/bar"));
545		let s = String::from("foo/bar");
546		let prefix3 = Path::from(&s);
547
548		assert_eq!(prefix1.as_str(), "foo/bar");
549		assert_eq!(prefix2.as_str(), "foo/bar");
550		assert_eq!(prefix3.as_str(), "foo/bar");
551	}
552
553	#[test]
554	fn test_path_suffix_conversions() {
555		let suffix1 = Path::from("foo/bar");
556		let suffix2 = Path::from(String::from("foo/bar"));
557		let s = String::from("foo/bar");
558		let suffix3 = Path::from(&s);
559
560		assert_eq!(suffix1.as_str(), "foo/bar");
561		assert_eq!(suffix2.as_str(), "foo/bar");
562		assert_eq!(suffix3.as_str(), "foo/bar");
563	}
564
565	#[test]
566	fn test_path_types_basic_operations() {
567		let prefix = Path::new("foo/bar");
568		assert_eq!(prefix.as_str(), "foo/bar");
569		assert!(!prefix.is_empty());
570		assert_eq!(prefix.len(), 7);
571
572		let suffix = Path::new("baz/qux");
573		assert_eq!(suffix.as_str(), "baz/qux");
574		assert!(!suffix.is_empty());
575		assert_eq!(suffix.len(), 7);
576
577		let empty_prefix = Path::new("");
578		assert!(empty_prefix.is_empty());
579		assert_eq!(empty_prefix.len(), 0);
580
581		let empty_suffix = Path::new("");
582		assert!(empty_suffix.is_empty());
583		assert_eq!(empty_suffix.len(), 0);
584	}
585
586	#[test]
587	fn test_prefix_has_prefix() {
588		// Test empty prefix (should match everything)
589		let prefix = Path::new("foo/bar");
590		assert!(prefix.has_prefix(""));
591
592		// Test exact matches
593		let prefix = Path::new("foo/bar");
594		assert!(prefix.has_prefix("foo/bar"));
595
596		// Test valid prefixes
597		assert!(prefix.has_prefix("foo"));
598		assert!(prefix.has_prefix("foo/"));
599
600		// Test invalid prefixes - partial matches should fail
601		assert!(!prefix.has_prefix("f"));
602		assert!(!prefix.has_prefix("fo"));
603		assert!(!prefix.has_prefix("foo/b"));
604		assert!(!prefix.has_prefix("foo/ba"));
605
606		// Test edge cases
607		let prefix = Path::new("foobar");
608		assert!(!prefix.has_prefix("foo"));
609		assert!(prefix.has_prefix("foobar"));
610
611		// Test trailing slash handling
612		let prefix = Path::new("foo/bar/");
613		assert!(prefix.has_prefix("foo"));
614		assert!(prefix.has_prefix("foo/"));
615		assert!(prefix.has_prefix("foo/bar"));
616		assert!(prefix.has_prefix("foo/bar/"));
617
618		// Test single component
619		let prefix = Path::new("foo");
620		assert!(prefix.has_prefix(""));
621		assert!(prefix.has_prefix("foo"));
622		assert!(prefix.has_prefix("foo/")); // "foo/" becomes "foo" after trimming
623		assert!(!prefix.has_prefix("f"));
624
625		// Test empty prefix
626		let prefix = Path::new("");
627		assert!(prefix.has_prefix(""));
628		assert!(!prefix.has_prefix("foo"));
629	}
630
631	#[test]
632	fn test_prefix_join() {
633		// Basic joining
634		let prefix = Path::new("foo");
635		let suffix = Path::new("bar");
636		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
637
638		// Trailing slash on prefix
639		let prefix = Path::new("foo/");
640		let suffix = Path::new("bar");
641		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
642
643		// Leading slash on suffix
644		let prefix = Path::new("foo");
645		let suffix = Path::new("/bar");
646		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
647
648		// Trailing slash on suffix
649		let prefix = Path::new("foo");
650		let suffix = Path::new("bar/");
651		assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); // trailing slash is trimmed
652
653		// Both have slashes
654		let prefix = Path::new("foo/");
655		let suffix = Path::new("/bar");
656		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
657
658		// Empty suffix
659		let prefix = Path::new("foo");
660		let suffix = Path::new("");
661		assert_eq!(prefix.join(suffix).as_str(), "foo");
662
663		// Empty prefix
664		let prefix = Path::new("");
665		let suffix = Path::new("bar");
666		assert_eq!(prefix.join(suffix).as_str(), "bar");
667
668		// Both empty
669		let prefix = Path::new("");
670		let suffix = Path::new("");
671		assert_eq!(prefix.join(suffix).as_str(), "");
672
673		// Complex paths
674		let prefix = Path::new("foo/bar");
675		let suffix = Path::new("baz/qux");
676		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux");
677
678		// Complex paths with slashes
679		let prefix = Path::new("foo/bar/");
680		let suffix = Path::new("/baz/qux/");
681		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
682	}
683
684	#[test]
685	fn test_path_ref() {
686		// Test PathRef creation and normalization
687		let ref1 = Path::new("/foo/bar/");
688		assert_eq!(ref1.as_str(), "foo/bar");
689
690		let ref2 = Path::from("///foo///");
691		assert_eq!(ref2.as_str(), "foo");
692
693		// Test PathRef normalizes multiple slashes
694		let ref3 = Path::new("foo//bar///baz");
695		assert_eq!(ref3.as_str(), "foo/bar/baz");
696
697		// Test conversions
698		let path = Path::new("foo/bar");
699		let path_ref = path;
700		assert_eq!(path_ref.as_str(), "foo/bar");
701
702		// Test that Path methods work with PathRef
703		let path2 = Path::new("foo/bar/baz");
704		assert!(path2.has_prefix(&path_ref));
705		assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz");
706
707		// Test empty PathRef
708		let empty = Path::new("");
709		assert!(empty.is_empty());
710		assert_eq!(empty.len(), 0);
711	}
712
713	#[test]
714	fn test_multiple_consecutive_slashes() {
715		let path = Path::new("foo//bar///baz");
716		// Multiple consecutive slashes are collapsed to single slashes
717		assert_eq!(path.as_str(), "foo/bar/baz");
718
719		// Test with leading and trailing slashes too
720		let path2 = Path::new("//foo//bar///baz//");
721		assert_eq!(path2.as_str(), "foo/bar/baz");
722
723		// Test empty segments are handled correctly
724		let path3 = Path::new("foo///bar");
725		assert_eq!(path3.as_str(), "foo/bar");
726	}
727
728	#[test]
729	fn test_removes_multiple_slashes_comprehensively() {
730		// Test various multiple slash scenarios
731		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
732		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
733		assert_eq!(Path::new("foo////bar").as_str(), "foo/bar");
734
735		// Multiple occurrences of double slashes
736		assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz");
737		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
738
739		// Mixed slash counts
740		assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux");
741
742		// With leading and trailing slashes
743		assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar");
744		assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar");
745
746		// Edge case: only slashes
747		assert_eq!(Path::new("//").as_str(), "");
748		assert_eq!(Path::new("////").as_str(), "");
749
750		// Test that operations work correctly with normalized paths
751		let path_with_slashes = Path::new("foo//bar///baz");
752		assert!(path_with_slashes.has_prefix("foo/bar"));
753		assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz");
754		assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux");
755
756		// Test PathRef to Path conversion
757		let path_ref = Path::new("foo//bar///baz");
758		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too
759		let path_from_ref = path_ref.to_owned();
760		assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized
761	}
762
763	#[test]
764	fn test_path_ref_multiple_slashes() {
765		// PathRef now normalizes multiple slashes using Cow
766		let path_ref = Path::new("//foo//bar///baz//");
767		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized
768
769		// Various multiple slash scenarios are normalized in PathRef
770		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
771		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
772		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
773
774		// Conversion to Path maintains normalized form
775		assert_eq!(Path::new("foo//bar").to_owned().as_str(), "foo/bar");
776		assert_eq!(Path::new("foo///bar").to_owned().as_str(), "foo/bar");
777		assert_eq!(Path::new("a//b//c//d").to_owned().as_str(), "a/b/c/d");
778
779		// Edge cases
780		assert_eq!(Path::new("//").as_str(), "");
781		assert_eq!(Path::new("////").as_str(), "");
782		assert_eq!(Path::new("//").to_owned().as_str(), "");
783		assert_eq!(Path::new("////").to_owned().as_str(), "");
784
785		// Test that PathRef avoids allocation when no normalization needed
786		let normal_path = Path::new("foo/bar/baz");
787		assert_eq!(normal_path.as_str(), "foo/bar/baz");
788		// This should use Cow::Borrowed internally (no allocation)
789
790		let needs_norm = Path::new("foo//bar");
791		assert_eq!(needs_norm.as_str(), "foo/bar");
792		// This should use Cow::Owned internally (allocation only when needed)
793	}
794
795	#[test]
796	fn test_ergonomic_conversions() {
797		// Test that all these work ergonomically in function calls
798		fn takes_path_ref<'a>(p: impl Into<Path<'a>>) -> String {
799			p.into().as_str().to_string()
800		}
801
802		// Alternative API using the trait alias for better error messages
803		fn takes_path_ref_with_trait<'a>(p: impl Into<Path<'a>>) -> String {
804			p.into().as_str().to_string()
805		}
806
807		// String literal
808		assert_eq!(takes_path_ref("foo//bar"), "foo/bar");
809
810		// String (owned) - this should now work without &
811		let owned_string = String::from("foo//bar///baz");
812		assert_eq!(takes_path_ref(owned_string), "foo/bar/baz");
813
814		// &String
815		let string_ref = String::from("foo//bar");
816		assert_eq!(takes_path_ref(string_ref), "foo/bar");
817
818		// PathRef
819		let path_ref = Path::new("foo//bar");
820		assert_eq!(takes_path_ref(path_ref), "foo/bar");
821
822		// Path
823		let path = Path::new("foo//bar");
824		assert_eq!(takes_path_ref(path), "foo/bar");
825
826		// Test that Path::new works with all these types
827		let _path1 = Path::new("foo/bar"); // &str
828		let _path2 = Path::new("foo/bar"); // String - should now work
829		let _path3 = Path::new("foo/bar"); // &String
830		let _path4 = Path::new("foo/bar"); // PathRef
831
832		// Test the trait alias version works the same
833		assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar");
834		assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar");
835	}
836
837	#[test]
838	fn test_prefix_strip_prefix() {
839		// Test basic stripping
840		let prefix = Path::new("foo/bar/baz");
841		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
842		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar/baz");
843		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar/baz");
844		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "baz");
845		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "baz");
846		assert_eq!(prefix.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
847
848		// Test invalid prefixes
849		assert!(prefix.strip_prefix("fo").is_none());
850		assert!(prefix.strip_prefix("bar").is_none());
851		assert!(prefix.strip_prefix("foo/ba").is_none());
852
853		// Test edge cases
854		let prefix = Path::new("foobar");
855		assert!(prefix.strip_prefix("foo").is_none());
856		assert_eq!(prefix.strip_prefix("foobar").unwrap().as_str(), "");
857
858		// Test empty prefix
859		let prefix = Path::new("");
860		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "");
861		assert!(prefix.strip_prefix("foo").is_none());
862
863		// Test single component
864		let prefix = Path::new("foo");
865		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "");
866		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
867
868		// Test trailing slash handling
869		let prefix = Path::new("foo/bar/");
870		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar");
871		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar");
872		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "");
873		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "");
874	}
875
876	#[test]
877	fn test_prefix_list_dedup() {
878		// Exact duplicates are removed
879		let list = PathPrefixes::new(["demo", "demo"]);
880		assert_eq!(list.len(), 1);
881		assert_eq!(list[0], Path::new("demo"));
882	}
883
884	#[test]
885	fn test_prefix_list_overlap() {
886		// "demo/foo" is redundant when "demo" exists
887		let list = PathPrefixes::new(["demo", "demo/foo", "anon"]);
888		assert_eq!(list.len(), 2);
889		assert!(list.iter().any(|p| p == &Path::new("demo")));
890		assert!(list.iter().any(|p| p == &Path::new("anon")));
891	}
892
893	#[test]
894	fn test_prefix_list_overlap_reverse_order() {
895		// Order shouldn't matter
896		let list = PathPrefixes::new(["demo/foo", "demo"]);
897		assert_eq!(list.len(), 1);
898		assert_eq!(list[0], Path::new("demo"));
899	}
900
901	#[test]
902	fn test_prefix_list_empty_covers_all() {
903		// Empty prefix covers everything
904		let list = PathPrefixes::new(["", "demo", "anon"]);
905		assert_eq!(list.len(), 1);
906		assert_eq!(list[0], Path::new(""));
907	}
908
909	#[test]
910	fn test_prefix_list_no_overlap() {
911		// Unrelated prefixes are all kept
912		let list = PathPrefixes::new(["demo", "anon", "secret"]);
913		assert_eq!(list.len(), 3);
914	}
915
916	#[test]
917	fn test_prefix_list_single() {
918		let list = PathPrefixes::new(["demo"]);
919		assert_eq!(list.len(), 1);
920	}
921
922	#[test]
923	fn test_prefix_list_empty() {
924		let list = PathPrefixes::new(std::iter::empty::<&str>());
925		assert!(list.is_empty());
926		assert_eq!(list.len(), 0);
927	}
928
929	#[test]
930	fn test_prefix_list_deep_overlap() {
931		// "a/b/c" is covered by "a/b" which is covered by "a"
932		let list = PathPrefixes::new(["a/b/c", "a/b", "a"]);
933		assert_eq!(list.len(), 1);
934		assert_eq!(list[0], Path::new("a"));
935	}
936
937	#[test]
938	fn test_prefix_list_partial_name_not_overlap() {
939		// "demo" should NOT cover "demonstration" (different path component)
940		let list = PathPrefixes::new(["demo", "demonstration"]);
941		assert_eq!(list.len(), 2);
942	}
943
944	#[test]
945	fn test_prefix_list_collect() {
946		let paths: Vec<PathOwned> = vec!["demo".into(), "demo/foo".into()];
947		let list: PathPrefixes = paths.into_iter().collect();
948		assert_eq!(list.len(), 1);
949		assert_eq!(list[0], Path::new("demo"));
950	}
951
952	#[test]
953	fn test_prefix_list_eq_vec() {
954		let list = PathPrefixes::new(["demo", "anon"]);
955		// Canonical order: sorted by length, then lexicographically
956		assert_eq!(list, vec!["anon".as_path(), "demo".as_path()]);
957	}
958
959	#[test]
960	fn test_prefix_list_canonical_order() {
961		// Same inputs in different order produce identical results
962		let a = PathPrefixes::new(["foo", "bar"]);
963		let b = PathPrefixes::new(["bar", "foo"]);
964		assert_eq!(a, b);
965	}
966}