moq_lite/
path.rs

1use std::borrow::Cow;
2use std::fmt::{self, Display};
3
4use crate::coding::{Decode, DecodeError, Encode};
5
6pub type PathOwned = Path<'static>;
7
8/// A trait for types that can be converted to a `Path`.
9///
10/// When providing a String/str, any leading/trailing slashes are trimmed and multiple consecutive slashes are collapsed.
11/// When already a Path, normalization is skipped as a reference is returned.
12pub trait AsPath {
13	fn as_path(&self) -> Path<'_>;
14}
15
16impl<'a> AsPath for &'a str {
17	fn as_path(&self) -> Path<'a> {
18		Path::new(self)
19	}
20}
21
22impl<'a> AsPath for &'a Path<'a> {
23	fn as_path(&self) -> Path<'a> {
24		// We don't normalize again nor do we make a copy.
25		Path(Cow::Borrowed(self.as_str()))
26	}
27}
28
29impl<'a> AsPath for Path<'a> {
30	fn as_path(&self) -> Path<'_> {
31		Path(Cow::Borrowed(self.0.as_ref()))
32	}
33}
34
35impl AsPath for String {
36	fn as_path(&self) -> Path<'_> {
37		Path(Cow::Borrowed(self))
38	}
39}
40
41impl<'a> AsPath for &'a String {
42	fn as_path(&self) -> Path<'a> {
43		Path(Cow::Borrowed(self))
44	}
45}
46
47/// A broadcast path that provides safe prefix matching operations.
48///
49/// This type wraps a String but provides path-aware operations that respect
50/// delimiter boundaries, preventing issues like "foo" matching "foobar".
51///
52/// Paths are automatically trimmed of leading and trailing slashes on creation,
53/// making all slashes implicit at boundaries.
54/// All paths are RELATIVE; you cannot join with a leading slash to make an absolute path.
55///
56/// # Examples
57/// ```
58/// use moq_lite::{Path};
59///
60/// // Creation automatically trims slashes
61/// let path1 = Path::new("/foo/bar/");
62/// let path2 = Path::new("foo/bar");
63/// assert_eq!(path1, path2);
64///
65/// // Methods accept both &str and Path
66/// let base = Path::new("api/v1");
67/// assert!(base.has_prefix("api"));
68/// assert!(base.has_prefix(&Path::new("api/v1")));
69///
70/// let joined = base.join("users");
71/// assert_eq!(joined.as_str(), "api/v1/users");
72/// ```
73#[derive(Debug, PartialEq, Eq, Hash, Clone)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize))]
75pub struct Path<'a>(Cow<'a, str>);
76
77impl<'a> Path<'a> {
78	/// Create a new Path from a string slice.
79	///
80	/// Leading and trailing slashes are automatically trimmed.
81	/// Multiple consecutive internal slashes are collapsed to single slashes.
82	pub fn new(s: &'a str) -> Self {
83		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
84
85		// Check if we need to normalize (has multiple consecutive slashes)
86		if trimmed.contains("//") {
87			// Only allocate if we actually need to normalize
88			let normalized = trimmed
89				.split('/')
90				.filter(|s| !s.is_empty())
91				.collect::<Vec<_>>()
92				.join("/");
93			Self(Cow::Owned(normalized))
94		} else {
95			// No normalization needed - use borrowed string
96			Self(Cow::Borrowed(trimmed))
97		}
98	}
99
100	/// Check if this path has the given prefix, respecting path boundaries.
101	///
102	/// Unlike String::starts_with, this ensures that "foo" does not match "foobar".
103	/// The prefix must either:
104	/// - Be exactly equal to this path
105	/// - Be followed by a '/' delimiter in the original path
106	/// - Be empty (matches everything)
107	///
108	/// # Examples
109	/// ```
110	/// use moq_lite::Path;
111	///
112	/// let path = Path::new("foo/bar");
113	/// assert!(path.has_prefix("foo"));
114	/// assert!(path.has_prefix(&Path::new("foo")));
115	/// assert!(path.has_prefix("foo/"));
116	/// assert!(!path.has_prefix("fo"));
117	///
118	/// let path = Path::new("foobar");
119	/// assert!(!path.has_prefix("foo"));
120	/// ```
121	pub fn has_prefix(&self, prefix: impl AsPath) -> bool {
122		let prefix = prefix.as_path();
123
124		if prefix.is_empty() {
125			return true;
126		}
127
128		if !self.0.starts_with(prefix.as_str()) {
129			return false;
130		}
131
132		// Check if the prefix is the exact match
133		if self.0.len() == prefix.len() {
134			return true;
135		}
136
137		// Otherwise, ensure the character after the prefix is a delimiter
138		self.0.chars().nth(prefix.len()) == Some('/')
139	}
140
141	pub fn strip_prefix(&'a self, prefix: impl AsPath) -> Option<Path<'a>> {
142		let prefix = prefix.as_path();
143
144		if prefix.is_empty() {
145			return Some(self.borrow());
146		}
147
148		if !self.0.starts_with(prefix.as_str()) {
149			return None;
150		}
151
152		// Check if the prefix is the exact match
153		if self.0.len() == prefix.len() {
154			return Some(Path(Cow::Borrowed("")));
155		}
156
157		// Otherwise, ensure the character after the prefix is a delimiter
158		if self.0.chars().nth(prefix.len()) != Some('/') {
159			return None;
160		}
161
162		Some(Path(Cow::Borrowed(&self.0[prefix.len() + 1..])))
163	}
164
165	/// Strip the directory component of the path, if any, and return the rest of the path.
166	pub fn next_part(&'a self) -> Option<(&'a str, Path<'a>)> {
167		if self.0.is_empty() {
168			return None;
169		}
170
171		if let Some(i) = self.0.find('/') {
172			let dir = &self.0[..i];
173			let rest = Path(Cow::Borrowed(&self.0[i + 1..]));
174			Some((dir, rest))
175		} else {
176			Some((&self.0, Path(Cow::Borrowed(""))))
177		}
178	}
179
180	pub fn as_str(&self) -> &str {
181		&self.0
182	}
183
184	pub fn empty() -> Path<'static> {
185		Path(Cow::Borrowed(""))
186	}
187
188	pub fn is_empty(&self) -> bool {
189		self.0.is_empty()
190	}
191
192	pub fn len(&self) -> usize {
193		self.0.len()
194	}
195
196	pub fn to_owned(&self) -> PathOwned {
197		Path(Cow::Owned(self.0.to_string()))
198	}
199
200	pub fn into_owned(self) -> PathOwned {
201		Path(Cow::Owned(self.0.to_string()))
202	}
203
204	pub fn borrow(&'a self) -> Path<'a> {
205		Path(Cow::Borrowed(&self.0))
206	}
207
208	/// Join this path with another path component.
209	///
210	/// # Examples
211	/// ```
212	/// use moq_lite::Path;
213	///
214	/// let base = Path::new("foo");
215	/// let joined = base.join("bar");
216	/// assert_eq!(joined.as_str(), "foo/bar");
217	///
218	/// let joined = base.join(&Path::new("bar"));
219	/// assert_eq!(joined.as_str(), "foo/bar");
220	/// ```
221	pub fn join(&self, other: impl AsPath) -> PathOwned {
222		let other = other.as_path();
223
224		if self.0.is_empty() {
225			Path(Cow::Owned(other.0.to_string()))
226		} else if other.is_empty() {
227			// Technically, we could avoid allocating here, but it's nicer to return a PathOwned.
228			self.to_owned()
229		} else {
230			// Since paths are trimmed, we always need to add a slash
231			Path(Cow::Owned(format!("{}/{}", self.0, other.as_str())))
232		}
233	}
234}
235
236impl<'a> From<&'a str> for Path<'a> {
237	fn from(s: &'a str) -> Self {
238		Self::new(s)
239	}
240}
241
242impl<'a> From<&'a String> for Path<'a> {
243	fn from(s: &'a String) -> Self {
244		// TODO avoid making a copy here
245		Self::new(s)
246	}
247}
248
249impl<'a> Default for Path<'a> {
250	fn default() -> Self {
251		Self(Cow::Borrowed(""))
252	}
253}
254
255impl<'a> From<String> for Path<'a> {
256	fn from(s: String) -> Self {
257		// It's annoying that this logic is duplicated, but I couldn't figure out how to reuse Path::new.
258		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
259
260		// Check if we need to normalize (has multiple consecutive slashes)
261		if trimmed.contains("//") {
262			// Only allocate if we actually need to normalize
263			let normalized = trimmed
264				.split('/')
265				.filter(|s| !s.is_empty())
266				.collect::<Vec<_>>()
267				.join("/");
268			Self(Cow::Owned(normalized))
269		} else if trimmed == s {
270			// String is already trimmed and normalized, use it directly
271			Self(Cow::Owned(s))
272		} else {
273			// Need to trim but don't need to normalize internal slashes
274			Self(Cow::Owned(trimmed.to_string()))
275		}
276	}
277}
278
279impl<'a> AsRef<str> for Path<'a> {
280	fn as_ref(&self) -> &str {
281		&self.0
282	}
283}
284
285impl<'a> Display for Path<'a> {
286	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287		write!(f, "{}", self.0)
288	}
289}
290
291impl<'a, V> Decode<V> for Path<'a> {
292	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
293		Ok(String::decode(r, version)?.into())
294	}
295}
296
297impl<'a, V> Encode<V> for Path<'a> {
298	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) {
299		self.as_str().encode(w, version)
300	}
301}
302
303// A custom deserializer is needed in order to sanitize
304#[cfg(feature = "serde")]
305impl<'de: 'a, 'a> serde::Deserialize<'de> for Path<'a> {
306	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
307	where
308		D: serde::Deserializer<'de>,
309	{
310		let s = <&'a str as serde::Deserialize<'de>>::deserialize(deserializer)?;
311		Ok(Path::new(s))
312	}
313}
314
315#[cfg(test)]
316mod tests {
317	use super::*;
318
319	#[test]
320	fn test_has_prefix() {
321		let path = Path::new("foo/bar/baz");
322
323		// Valid prefixes - test with both &str and &Path
324		assert!(path.has_prefix(""));
325		assert!(path.has_prefix("foo"));
326		assert!(path.has_prefix(Path::new("foo")));
327		assert!(path.has_prefix("foo/"));
328		assert!(path.has_prefix("foo/bar"));
329		assert!(path.has_prefix(Path::new("foo/bar/")));
330		assert!(path.has_prefix("foo/bar/baz"));
331
332		// Invalid prefixes - should not match partial components
333		assert!(!path.has_prefix("f"));
334		assert!(!path.has_prefix(Path::new("fo")));
335		assert!(!path.has_prefix("foo/b"));
336		assert!(!path.has_prefix("foo/ba"));
337		assert!(!path.has_prefix(Path::new("foo/bar/ba")));
338
339		// Edge case: "foobar" should not match "foo"
340		let path = Path::new("foobar");
341		assert!(!path.has_prefix("foo"));
342		assert!(path.has_prefix(Path::new("foobar")));
343	}
344
345	#[test]
346	fn test_strip_prefix() {
347		let path = Path::new("foo/bar/baz");
348
349		// Test with both &str and &Path
350		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
351		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
352		assert_eq!(path.strip_prefix(Path::new("foo/")).unwrap().as_str(), "bar/baz");
353		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
354		assert_eq!(path.strip_prefix(Path::new("foo/bar/")).unwrap().as_str(), "baz");
355		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
356
357		// Should fail for invalid prefixes
358		assert!(path.strip_prefix("fo").is_none());
359		assert!(path.strip_prefix(Path::new("bar")).is_none());
360	}
361
362	#[test]
363	fn test_join() {
364		// Test with both &str and &Path
365		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
366		assert_eq!(Path::new("foo/").join(Path::new("bar")).as_str(), "foo/bar");
367		assert_eq!(Path::new("").join("bar").as_str(), "bar");
368		assert_eq!(Path::new("foo/bar").join(Path::new("baz")).as_str(), "foo/bar/baz");
369	}
370
371	#[test]
372	fn test_empty() {
373		let empty = Path::new("");
374		assert!(empty.is_empty());
375		assert_eq!(empty.len(), 0);
376
377		let non_empty = Path::new("foo");
378		assert!(!non_empty.is_empty());
379		assert_eq!(non_empty.len(), 3);
380	}
381
382	#[test]
383	fn test_from_conversions() {
384		let path1 = Path::from("foo/bar");
385		let path2 = Path::from("foo/bar");
386		let s = String::from("foo/bar");
387		let path3 = Path::from(&s);
388
389		assert_eq!(path1.as_str(), "foo/bar");
390		assert_eq!(path2.as_str(), "foo/bar");
391		assert_eq!(path3.as_str(), "foo/bar");
392	}
393
394	#[test]
395	fn test_path_prefix_join() {
396		let prefix = Path::new("foo");
397		let suffix = Path::new("bar/baz");
398		let path = prefix.join(&suffix);
399		assert_eq!(path.as_str(), "foo/bar/baz");
400
401		let prefix = Path::new("foo/");
402		let suffix = Path::new("bar/baz");
403		let path = prefix.join(&suffix);
404		assert_eq!(path.as_str(), "foo/bar/baz");
405
406		let prefix = Path::new("foo");
407		let suffix = Path::new("/bar/baz");
408		let path = prefix.join(&suffix);
409		assert_eq!(path.as_str(), "foo/bar/baz");
410
411		let prefix = Path::new("");
412		let suffix = Path::new("bar/baz");
413		let path = prefix.join(&suffix);
414		assert_eq!(path.as_str(), "bar/baz");
415	}
416
417	#[test]
418	fn test_path_prefix_conversions() {
419		let prefix1 = Path::from("foo/bar");
420		let prefix2 = Path::from(String::from("foo/bar"));
421		let s = String::from("foo/bar");
422		let prefix3 = Path::from(&s);
423
424		assert_eq!(prefix1.as_str(), "foo/bar");
425		assert_eq!(prefix2.as_str(), "foo/bar");
426		assert_eq!(prefix3.as_str(), "foo/bar");
427	}
428
429	#[test]
430	fn test_path_suffix_conversions() {
431		let suffix1 = Path::from("foo/bar");
432		let suffix2 = Path::from(String::from("foo/bar"));
433		let s = String::from("foo/bar");
434		let suffix3 = Path::from(&s);
435
436		assert_eq!(suffix1.as_str(), "foo/bar");
437		assert_eq!(suffix2.as_str(), "foo/bar");
438		assert_eq!(suffix3.as_str(), "foo/bar");
439	}
440
441	#[test]
442	fn test_path_types_basic_operations() {
443		let prefix = Path::new("foo/bar");
444		assert_eq!(prefix.as_str(), "foo/bar");
445		assert!(!prefix.is_empty());
446		assert_eq!(prefix.len(), 7);
447
448		let suffix = Path::new("baz/qux");
449		assert_eq!(suffix.as_str(), "baz/qux");
450		assert!(!suffix.is_empty());
451		assert_eq!(suffix.len(), 7);
452
453		let empty_prefix = Path::new("");
454		assert!(empty_prefix.is_empty());
455		assert_eq!(empty_prefix.len(), 0);
456
457		let empty_suffix = Path::new("");
458		assert!(empty_suffix.is_empty());
459		assert_eq!(empty_suffix.len(), 0);
460	}
461
462	#[test]
463	fn test_prefix_has_prefix() {
464		// Test empty prefix (should match everything)
465		let prefix = Path::new("foo/bar");
466		assert!(prefix.has_prefix(""));
467
468		// Test exact matches
469		let prefix = Path::new("foo/bar");
470		assert!(prefix.has_prefix("foo/bar"));
471
472		// Test valid prefixes
473		assert!(prefix.has_prefix("foo"));
474		assert!(prefix.has_prefix("foo/"));
475
476		// Test invalid prefixes - partial matches should fail
477		assert!(!prefix.has_prefix("f"));
478		assert!(!prefix.has_prefix("fo"));
479		assert!(!prefix.has_prefix("foo/b"));
480		assert!(!prefix.has_prefix("foo/ba"));
481
482		// Test edge cases
483		let prefix = Path::new("foobar");
484		assert!(!prefix.has_prefix("foo"));
485		assert!(prefix.has_prefix("foobar"));
486
487		// Test trailing slash handling
488		let prefix = Path::new("foo/bar/");
489		assert!(prefix.has_prefix("foo"));
490		assert!(prefix.has_prefix("foo/"));
491		assert!(prefix.has_prefix("foo/bar"));
492		assert!(prefix.has_prefix("foo/bar/"));
493
494		// Test single component
495		let prefix = Path::new("foo");
496		assert!(prefix.has_prefix(""));
497		assert!(prefix.has_prefix("foo"));
498		assert!(prefix.has_prefix("foo/")); // "foo/" becomes "foo" after trimming
499		assert!(!prefix.has_prefix("f"));
500
501		// Test empty prefix
502		let prefix = Path::new("");
503		assert!(prefix.has_prefix(""));
504		assert!(!prefix.has_prefix("foo"));
505	}
506
507	#[test]
508	fn test_prefix_join() {
509		// Basic joining
510		let prefix = Path::new("foo");
511		let suffix = Path::new("bar");
512		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
513
514		// Trailing slash on prefix
515		let prefix = Path::new("foo/");
516		let suffix = Path::new("bar");
517		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
518
519		// Leading slash on suffix
520		let prefix = Path::new("foo");
521		let suffix = Path::new("/bar");
522		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
523
524		// Trailing slash on suffix
525		let prefix = Path::new("foo");
526		let suffix = Path::new("bar/");
527		assert_eq!(prefix.join(suffix).as_str(), "foo/bar"); // trailing slash is trimmed
528
529		// Both have slashes
530		let prefix = Path::new("foo/");
531		let suffix = Path::new("/bar");
532		assert_eq!(prefix.join(suffix).as_str(), "foo/bar");
533
534		// Empty suffix
535		let prefix = Path::new("foo");
536		let suffix = Path::new("");
537		assert_eq!(prefix.join(suffix).as_str(), "foo");
538
539		// Empty prefix
540		let prefix = Path::new("");
541		let suffix = Path::new("bar");
542		assert_eq!(prefix.join(suffix).as_str(), "bar");
543
544		// Both empty
545		let prefix = Path::new("");
546		let suffix = Path::new("");
547		assert_eq!(prefix.join(suffix).as_str(), "");
548
549		// Complex paths
550		let prefix = Path::new("foo/bar");
551		let suffix = Path::new("baz/qux");
552		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux");
553
554		// Complex paths with slashes
555		let prefix = Path::new("foo/bar/");
556		let suffix = Path::new("/baz/qux/");
557		assert_eq!(prefix.join(suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
558	}
559
560	#[test]
561	fn test_path_ref() {
562		// Test PathRef creation and normalization
563		let ref1 = Path::new("/foo/bar/");
564		assert_eq!(ref1.as_str(), "foo/bar");
565
566		let ref2 = Path::from("///foo///");
567		assert_eq!(ref2.as_str(), "foo");
568
569		// Test PathRef normalizes multiple slashes
570		let ref3 = Path::new("foo//bar///baz");
571		assert_eq!(ref3.as_str(), "foo/bar/baz");
572
573		// Test conversions
574		let path = Path::new("foo/bar");
575		let path_ref = path;
576		assert_eq!(path_ref.as_str(), "foo/bar");
577
578		// Test that Path methods work with PathRef
579		let path2 = Path::new("foo/bar/baz");
580		assert!(path2.has_prefix(&path_ref));
581		assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz");
582
583		// Test empty PathRef
584		let empty = Path::new("");
585		assert!(empty.is_empty());
586		assert_eq!(empty.len(), 0);
587	}
588
589	#[test]
590	fn test_multiple_consecutive_slashes() {
591		let path = Path::new("foo//bar///baz");
592		// Multiple consecutive slashes are collapsed to single slashes
593		assert_eq!(path.as_str(), "foo/bar/baz");
594
595		// Test with leading and trailing slashes too
596		let path2 = Path::new("//foo//bar///baz//");
597		assert_eq!(path2.as_str(), "foo/bar/baz");
598
599		// Test empty segments are handled correctly
600		let path3 = Path::new("foo///bar");
601		assert_eq!(path3.as_str(), "foo/bar");
602	}
603
604	#[test]
605	fn test_removes_multiple_slashes_comprehensively() {
606		// Test various multiple slash scenarios
607		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
608		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
609		assert_eq!(Path::new("foo////bar").as_str(), "foo/bar");
610
611		// Multiple occurrences of double slashes
612		assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz");
613		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
614
615		// Mixed slash counts
616		assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux");
617
618		// With leading and trailing slashes
619		assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar");
620		assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar");
621
622		// Edge case: only slashes
623		assert_eq!(Path::new("//").as_str(), "");
624		assert_eq!(Path::new("////").as_str(), "");
625
626		// Test that operations work correctly with normalized paths
627		let path_with_slashes = Path::new("foo//bar///baz");
628		assert!(path_with_slashes.has_prefix("foo/bar"));
629		assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz");
630		assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux");
631
632		// Test PathRef to Path conversion
633		let path_ref = Path::new("foo//bar///baz");
634		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too
635		let path_from_ref = path_ref.to_owned();
636		assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized
637	}
638
639	#[test]
640	fn test_path_ref_multiple_slashes() {
641		// PathRef now normalizes multiple slashes using Cow
642		let path_ref = Path::new("//foo//bar///baz//");
643		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized
644
645		// Various multiple slash scenarios are normalized in PathRef
646		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
647		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
648		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
649
650		// Conversion to Path maintains normalized form
651		assert_eq!(Path::new("foo//bar").to_owned().as_str(), "foo/bar");
652		assert_eq!(Path::new("foo///bar").to_owned().as_str(), "foo/bar");
653		assert_eq!(Path::new("a//b//c//d").to_owned().as_str(), "a/b/c/d");
654
655		// Edge cases
656		assert_eq!(Path::new("//").as_str(), "");
657		assert_eq!(Path::new("////").as_str(), "");
658		assert_eq!(Path::new("//").to_owned().as_str(), "");
659		assert_eq!(Path::new("////").to_owned().as_str(), "");
660
661		// Test that PathRef avoids allocation when no normalization needed
662		let normal_path = Path::new("foo/bar/baz");
663		assert_eq!(normal_path.as_str(), "foo/bar/baz");
664		// This should use Cow::Borrowed internally (no allocation)
665
666		let needs_norm = Path::new("foo//bar");
667		assert_eq!(needs_norm.as_str(), "foo/bar");
668		// This should use Cow::Owned internally (allocation only when needed)
669	}
670
671	#[test]
672	fn test_ergonomic_conversions() {
673		// Test that all these work ergonomically in function calls
674		fn takes_path_ref<'a>(p: impl Into<Path<'a>>) -> String {
675			p.into().as_str().to_string()
676		}
677
678		// Alternative API using the trait alias for better error messages
679		fn takes_path_ref_with_trait<'a>(p: impl Into<Path<'a>>) -> String {
680			p.into().as_str().to_string()
681		}
682
683		// String literal
684		assert_eq!(takes_path_ref("foo//bar"), "foo/bar");
685
686		// String (owned) - this should now work without &
687		let owned_string = String::from("foo//bar///baz");
688		assert_eq!(takes_path_ref(owned_string), "foo/bar/baz");
689
690		// &String
691		let string_ref = String::from("foo//bar");
692		assert_eq!(takes_path_ref(string_ref), "foo/bar");
693
694		// PathRef
695		let path_ref = Path::new("foo//bar");
696		assert_eq!(takes_path_ref(path_ref), "foo/bar");
697
698		// Path
699		let path = Path::new("foo//bar");
700		assert_eq!(takes_path_ref(path), "foo/bar");
701
702		// Test that Path::new works with all these types
703		let _path1 = Path::new("foo/bar"); // &str
704		let _path2 = Path::new("foo/bar"); // String - should now work
705		let _path3 = Path::new("foo/bar"); // &String
706		let _path4 = Path::new("foo/bar"); // PathRef
707
708		// Test the trait alias version works the same
709		assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar");
710		assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar");
711	}
712
713	#[test]
714	fn test_prefix_strip_prefix() {
715		// Test basic stripping
716		let prefix = Path::new("foo/bar/baz");
717		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
718		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar/baz");
719		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar/baz");
720		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "baz");
721		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "baz");
722		assert_eq!(prefix.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
723
724		// Test invalid prefixes
725		assert!(prefix.strip_prefix("fo").is_none());
726		assert!(prefix.strip_prefix("bar").is_none());
727		assert!(prefix.strip_prefix("foo/ba").is_none());
728
729		// Test edge cases
730		let prefix = Path::new("foobar");
731		assert!(prefix.strip_prefix("foo").is_none());
732		assert_eq!(prefix.strip_prefix("foobar").unwrap().as_str(), "");
733
734		// Test empty prefix
735		let prefix = Path::new("");
736		assert_eq!(prefix.strip_prefix("").unwrap().as_str(), "");
737		assert!(prefix.strip_prefix("foo").is_none());
738
739		// Test single component
740		let prefix = Path::new("foo");
741		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "");
742		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
743
744		// Test trailing slash handling
745		let prefix = Path::new("foo/bar/");
746		assert_eq!(prefix.strip_prefix("foo").unwrap().as_str(), "bar");
747		assert_eq!(prefix.strip_prefix("foo/").unwrap().as_str(), "bar");
748		assert_eq!(prefix.strip_prefix("foo/bar").unwrap().as_str(), "");
749		assert_eq!(prefix.strip_prefix("foo/bar/").unwrap().as_str(), "");
750	}
751}