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.chars().nth(prefix.len()) == Some('/')
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.chars().nth(prefix.len()) != Some('/') {
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> Decode<V> for Path<'_> {
293	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
294		Ok(String::decode(r, version)?.into())
295	}
296}
297
298impl<V> Encode<V> for Path<'_> {
299	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
300		self.as_str().encode(w, version)?;
301		Ok(())
302	}
303}
304
305// A custom deserializer is needed in order to sanitize
306#[cfg(feature = "serde")]
307impl<'de: 'a, 'a> serde::Deserialize<'de> for Path<'a> {
308	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
309	where
310		D: serde::Deserializer<'de>,
311	{
312		let s = <&'a str as serde::Deserialize<'de>>::deserialize(deserializer)?;
313		Ok(Path::new(s))
314	}
315}
316
317#[cfg(test)]
318mod tests {
319	use super::*;
320
321	#[test]
322	fn test_has_prefix() {
323		let path = Path::new("foo/bar/baz");
324
325		// Valid prefixes - test with both &str and &Path
326		assert!(path.has_prefix(""));
327		assert!(path.has_prefix("foo"));
328		assert!(path.has_prefix(Path::new("foo")));
329		assert!(path.has_prefix("foo/"));
330		assert!(path.has_prefix("foo/bar"));
331		assert!(path.has_prefix(Path::new("foo/bar/")));
332		assert!(path.has_prefix("foo/bar/baz"));
333
334		// Invalid prefixes - should not match partial components
335		assert!(!path.has_prefix("f"));
336		assert!(!path.has_prefix(Path::new("fo")));
337		assert!(!path.has_prefix("foo/b"));
338		assert!(!path.has_prefix("foo/ba"));
339		assert!(!path.has_prefix(Path::new("foo/bar/ba")));
340
341		// Edge case: "foobar" should not match "foo"
342		let path = Path::new("foobar");
343		assert!(!path.has_prefix("foo"));
344		assert!(path.has_prefix(Path::new("foobar")));
345	}
346
347	#[test]
348	fn test_strip_prefix() {
349		let path = Path::new("foo/bar/baz");
350
351		// Test with both &str and &Path
352		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
353		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
354		assert_eq!(path.strip_prefix(Path::new("foo/")).unwrap().as_str(), "bar/baz");
355		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
356		assert_eq!(path.strip_prefix(Path::new("foo/bar/")).unwrap().as_str(), "baz");
357		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
358
359		// Should fail for invalid prefixes
360		assert!(path.strip_prefix("fo").is_none());
361		assert!(path.strip_prefix(Path::new("bar")).is_none());
362	}
363
364	#[test]
365	fn test_join() {
366		// Test with both &str and &Path
367		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
368		assert_eq!(Path::new("foo/").join(Path::new("bar")).as_str(), "foo/bar");
369		assert_eq!(Path::new("").join("bar").as_str(), "bar");
370		assert_eq!(Path::new("foo/bar").join(Path::new("baz")).as_str(), "foo/bar/baz");
371	}
372
373	#[test]
374	fn test_empty() {
375		let empty = Path::new("");
376		assert!(empty.is_empty());
377		assert_eq!(empty.len(), 0);
378
379		let non_empty = Path::new("foo");
380		assert!(!non_empty.is_empty());
381		assert_eq!(non_empty.len(), 3);
382	}
383
384	#[test]
385	fn test_from_conversions() {
386		let path1 = Path::from("foo/bar");
387		let path2 = Path::from("foo/bar");
388		let s = String::from("foo/bar");
389		let path3 = Path::from(&s);
390
391		assert_eq!(path1.as_str(), "foo/bar");
392		assert_eq!(path2.as_str(), "foo/bar");
393		assert_eq!(path3.as_str(), "foo/bar");
394	}
395
396	#[test]
397	fn test_path_prefix_join() {
398		let prefix = Path::new("foo");
399		let suffix = Path::new("bar/baz");
400		let path = prefix.join(&suffix);
401		assert_eq!(path.as_str(), "foo/bar/baz");
402
403		let prefix = Path::new("foo/");
404		let suffix = Path::new("bar/baz");
405		let path = prefix.join(&suffix);
406		assert_eq!(path.as_str(), "foo/bar/baz");
407
408		let prefix = Path::new("foo");
409		let suffix = Path::new("/bar/baz");
410		let path = prefix.join(&suffix);
411		assert_eq!(path.as_str(), "foo/bar/baz");
412
413		let prefix = Path::new("");
414		let suffix = Path::new("bar/baz");
415		let path = prefix.join(&suffix);
416		assert_eq!(path.as_str(), "bar/baz");
417	}
418
419	#[test]
420	fn test_path_prefix_conversions() {
421		let prefix1 = Path::from("foo/bar");
422		let prefix2 = Path::from(String::from("foo/bar"));
423		let s = String::from("foo/bar");
424		let prefix3 = Path::from(&s);
425
426		assert_eq!(prefix1.as_str(), "foo/bar");
427		assert_eq!(prefix2.as_str(), "foo/bar");
428		assert_eq!(prefix3.as_str(), "foo/bar");
429	}
430
431	#[test]
432	fn test_path_suffix_conversions() {
433		let suffix1 = Path::from("foo/bar");
434		let suffix2 = Path::from(String::from("foo/bar"));
435		let s = String::from("foo/bar");
436		let suffix3 = Path::from(&s);
437
438		assert_eq!(suffix1.as_str(), "foo/bar");
439		assert_eq!(suffix2.as_str(), "foo/bar");
440		assert_eq!(suffix3.as_str(), "foo/bar");
441	}
442
443	#[test]
444	fn test_path_types_basic_operations() {
445		let prefix = Path::new("foo/bar");
446		assert_eq!(prefix.as_str(), "foo/bar");
447		assert!(!prefix.is_empty());
448		assert_eq!(prefix.len(), 7);
449
450		let suffix = Path::new("baz/qux");
451		assert_eq!(suffix.as_str(), "baz/qux");
452		assert!(!suffix.is_empty());
453		assert_eq!(suffix.len(), 7);
454
455		let empty_prefix = Path::new("");
456		assert!(empty_prefix.is_empty());
457		assert_eq!(empty_prefix.len(), 0);
458
459		let empty_suffix = Path::new("");
460		assert!(empty_suffix.is_empty());
461		assert_eq!(empty_suffix.len(), 0);
462	}
463
464	#[test]
465	fn test_prefix_has_prefix() {
466		// Test empty prefix (should match everything)
467		let prefix = Path::new("foo/bar");
468		assert!(prefix.has_prefix(""));
469
470		// Test exact matches
471		let prefix = Path::new("foo/bar");
472		assert!(prefix.has_prefix("foo/bar"));
473
474		// Test valid prefixes
475		assert!(prefix.has_prefix("foo"));
476		assert!(prefix.has_prefix("foo/"));
477
478		// Test invalid prefixes - partial matches should fail
479		assert!(!prefix.has_prefix("f"));
480		assert!(!prefix.has_prefix("fo"));
481		assert!(!prefix.has_prefix("foo/b"));
482		assert!(!prefix.has_prefix("foo/ba"));
483
484		// Test edge cases
485		let prefix = Path::new("foobar");
486		assert!(!prefix.has_prefix("foo"));
487		assert!(prefix.has_prefix("foobar"));
488
489		// Test trailing slash handling
490		let prefix = Path::new("foo/bar/");
491		assert!(prefix.has_prefix("foo"));
492		assert!(prefix.has_prefix("foo/"));
493		assert!(prefix.has_prefix("foo/bar"));
494		assert!(prefix.has_prefix("foo/bar/"));
495
496		// Test single component
497		let prefix = Path::new("foo");
498		assert!(prefix.has_prefix(""));
499		assert!(prefix.has_prefix("foo"));
500		assert!(prefix.has_prefix("foo/")); // "foo/" becomes "foo" after trimming
501		assert!(!prefix.has_prefix("f"));
502
503		// Test empty prefix
504		let prefix = Path::new("");
505		assert!(prefix.has_prefix(""));
506		assert!(!prefix.has_prefix("foo"));
507	}
508
509	#[test]
510	fn test_prefix_join() {
511		// Basic joining
512		let prefix = Path::new("foo");
513		let suffix = Path::new("bar");
514		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
515
516		// Trailing slash on prefix
517		let prefix = Path::new("foo/");
518		let suffix = Path::new("bar");
519		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
520
521		// Leading slash on suffix
522		let prefix = Path::new("foo");
523		let suffix = Path::new("/bar");
524		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
525
526		// Trailing slash on suffix
527		let prefix = Path::new("foo");
528		let suffix = Path::new("bar/");
529		assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); // trailing slash is trimmed
530
531		// Both have slashes
532		let prefix = Path::new("foo/");
533		let suffix = Path::new("/bar");
534		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
535
536		// Empty suffix
537		let prefix = Path::new("foo");
538		let suffix = Path::new("");
539		assert_eq!(prefix.join(suffix).as_str(), "foo");
540
541		// Empty prefix
542		let prefix = Path::new("");
543		let suffix = Path::new("bar");
544		assert_eq!(prefix.join(suffix).as_str(), "bar");
545
546		// Both empty
547		let prefix = Path::new("");
548		let suffix = Path::new("");
549		assert_eq!(prefix.join(suffix).as_str(), "");
550
551		// Complex paths
552		let prefix = Path::new("foo/bar");
553		let suffix = Path::new("baz/qux");
554		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux");
555
556		// Complex paths with slashes
557		let prefix = Path::new("foo/bar/");
558		let suffix = Path::new("/baz/qux/");
559		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
560	}
561
562	#[test]
563	fn test_path_ref() {
564		// Test PathRef creation and normalization
565		let ref1 = Path::new("/foo/bar/");
566		assert_eq!(ref1.as_str(), "foo/bar");
567
568		let ref2 = Path::from("///foo///");
569		assert_eq!(ref2.as_str(), "foo");
570
571		// Test PathRef normalizes multiple slashes
572		let ref3 = Path::new("foo//bar///baz");
573		assert_eq!(ref3.as_str(), "foo/bar/baz");
574
575		// Test conversions
576		let path = Path::new("foo/bar");
577		let path_ref = path;
578		assert_eq!(path_ref.as_str(), "foo/bar");
579
580		// Test that Path methods work with PathRef
581		let path2 = Path::new("foo/bar/baz");
582		assert!(path2.has_prefix(&path_ref));
583		assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz");
584
585		// Test empty PathRef
586		let empty = Path::new("");
587		assert!(empty.is_empty());
588		assert_eq!(empty.len(), 0);
589	}
590
591	#[test]
592	fn test_multiple_consecutive_slashes() {
593		let path = Path::new("foo//bar///baz");
594		// Multiple consecutive slashes are collapsed to single slashes
595		assert_eq!(path.as_str(), "foo/bar/baz");
596
597		// Test with leading and trailing slashes too
598		let path2 = Path::new("//foo//bar///baz//");
599		assert_eq!(path2.as_str(), "foo/bar/baz");
600
601		// Test empty segments are handled correctly
602		let path3 = Path::new("foo///bar");
603		assert_eq!(path3.as_str(), "foo/bar");
604	}
605
606	#[test]
607	fn test_removes_multiple_slashes_comprehensively() {
608		// Test various multiple slash scenarios
609		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
610		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
611		assert_eq!(Path::new("foo////bar").as_str(), "foo/bar");
612
613		// Multiple occurrences of double slashes
614		assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz");
615		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
616
617		// Mixed slash counts
618		assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux");
619
620		// With leading and trailing slashes
621		assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar");
622		assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar");
623
624		// Edge case: only slashes
625		assert_eq!(Path::new("//").as_str(), "");
626		assert_eq!(Path::new("////").as_str(), "");
627
628		// Test that operations work correctly with normalized paths
629		let path_with_slashes = Path::new("foo//bar///baz");
630		assert!(path_with_slashes.has_prefix("foo/bar"));
631		assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz");
632		assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux");
633
634		// Test PathRef to Path conversion
635		let path_ref = Path::new("foo//bar///baz");
636		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too
637		let path_from_ref = path_ref.to_owned();
638		assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized
639	}
640
641	#[test]
642	fn test_path_ref_multiple_slashes() {
643		// PathRef now normalizes multiple slashes using Cow
644		let path_ref = Path::new("//foo//bar///baz//");
645		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized
646
647		// Various multiple slash scenarios are normalized in PathRef
648		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
649		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
650		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
651
652		// Conversion to Path maintains normalized form
653		assert_eq!(Path::new("foo//bar").to_owned().as_str(), "foo/bar");
654		assert_eq!(Path::new("foo///bar").to_owned().as_str(), "foo/bar");
655		assert_eq!(Path::new("a//b//c//d").to_owned().as_str(), "a/b/c/d");
656
657		// Edge cases
658		assert_eq!(Path::new("//").as_str(), "");
659		assert_eq!(Path::new("////").as_str(), "");
660		assert_eq!(Path::new("//").to_owned().as_str(), "");
661		assert_eq!(Path::new("////").to_owned().as_str(), "");
662
663		// Test that PathRef avoids allocation when no normalization needed
664		let normal_path = Path::new("foo/bar/baz");
665		assert_eq!(normal_path.as_str(), "foo/bar/baz");
666		// This should use Cow::Borrowed internally (no allocation)
667
668		let needs_norm = Path::new("foo//bar");
669		assert_eq!(needs_norm.as_str(), "foo/bar");
670		// This should use Cow::Owned internally (allocation only when needed)
671	}
672
673	#[test]
674	fn test_ergonomic_conversions() {
675		// Test that all these work ergonomically in function calls
676		fn takes_path_ref<'a>(p: impl Into<Path<'a>>) -> String {
677			p.into().as_str().to_string()
678		}
679
680		// Alternative API using the trait alias for better error messages
681		fn takes_path_ref_with_trait<'a>(p: impl Into<Path<'a>>) -> String {
682			p.into().as_str().to_string()
683		}
684
685		// String literal
686		assert_eq!(takes_path_ref("foo//bar"), "foo/bar");
687
688		// String (owned) - this should now work without &
689		let owned_string = String::from("foo//bar///baz");
690		assert_eq!(takes_path_ref(owned_string), "foo/bar/baz");
691
692		// &String
693		let string_ref = String::from("foo//bar");
694		assert_eq!(takes_path_ref(string_ref), "foo/bar");
695
696		// PathRef
697		let path_ref = Path::new("foo//bar");
698		assert_eq!(takes_path_ref(path_ref), "foo/bar");
699
700		// Path
701		let path = Path::new("foo//bar");
702		assert_eq!(takes_path_ref(path), "foo/bar");
703
704		// Test that Path::new works with all these types
705		let _path1 = Path::new("foo/bar"); // &str
706		let _path2 = Path::new("foo/bar"); // String - should now work
707		let _path3 = Path::new("foo/bar"); // &String
708		let _path4 = Path::new("foo/bar"); // PathRef
709
710		// Test the trait alias version works the same
711		assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar");
712		assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar");
713	}
714
715	#[test]
716	fn test_prefix_strip_prefix() {
717		// Test basic stripping
718		let prefix = Path::new("foo/bar/baz");
719		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
720		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar/baz");
721		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar/baz");
722		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "baz");
723		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "baz");
724		assert_eq!(prefix.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
725
726		// Test invalid prefixes
727		assert!(prefix.strip_prefix("fo").is_none());
728		assert!(prefix.strip_prefix("bar").is_none());
729		assert!(prefix.strip_prefix("foo/ba").is_none());
730
731		// Test edge cases
732		let prefix = Path::new("foobar");
733		assert!(prefix.strip_prefix("foo").is_none());
734		assert_eq!(prefix.strip_prefix("foobar").unwrap().as_str(), "");
735
736		// Test empty prefix
737		let prefix = Path::new("");
738		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "");
739		assert!(prefix.strip_prefix("foo").is_none());
740
741		// Test single component
742		let prefix = Path::new("foo");
743		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "");
744		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
745
746		// Test trailing slash handling
747		let prefix = Path::new("foo/bar/");
748		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar");
749		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar");
750		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "");
751		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "");
752	}
753}