Skip to main content

normalized_path/
path_element.rs

1use alloc::borrow::Cow;
2use alloc::string::String;
3#[cfg(feature = "std")]
4use std::ffi::{OsStr, OsString};
5
6use crate::Result;
7use crate::case_sensitivity::{CaseInsensitive, CaseSensitive, CaseSensitivity};
8use crate::error::ErrorKind;
9use crate::normalize::{normalize_ci_from_normalized_cs, normalize_cs};
10use crate::os::os_compatible_from_normalized_cs;
11use crate::utils::SubstringOrOwned;
12
13/// A validated, normalized, **case-sensitive** single path element.
14///
15/// Takes a raw path element name, validates it (rejecting empty strings, `.`, `..`,
16/// strings containing `/` or `\0`, and unassigned Unicode characters), normalizes it, and computes an OS-compatible
17/// presentation form.
18///
19/// Equality, ordering, and hashing are based on the **normalized** form.
20/// Case is preserved -- `"Hello.txt"` and `"hello.txt"` are distinct.
21/// Implements [`Hash`](core::hash::Hash).
22pub type PathElementCS<'a> = PathElementGeneric<'a, CaseSensitive>;
23
24/// A validated, normalized, **case-insensitive** single path element.
25///
26/// Takes a raw path element name, validates it (rejecting empty strings, `.`, `..`,
27/// strings containing `/` or `\0`, and unassigned Unicode characters), normalizes it case-insensitively, and computes
28/// an OS-compatible presentation form.
29///
30/// Equality, ordering, and hashing are based on the **normalized** (case-folded) form.
31/// `"Hello.txt"` and `"hello.txt"` are equal.
32/// Implements [`Hash`](core::hash::Hash).
33pub type PathElementCI<'a> = PathElementGeneric<'a, CaseInsensitive>;
34
35/// A validated, normalized single path element with **runtime-selected** case sensitivity.
36///
37/// Takes a raw path element name, validates it (rejecting empty strings, `.`, `..`,
38/// strings containing `/` or `\0`, and unassigned Unicode characters), normalizes it, and computes an OS-compatible
39/// presentation form.
40/// Case sensitivity is chosen at construction time via the [`CaseSensitivity`] enum.
41///
42/// Does **not** implement [`Hash`](core::hash::Hash) because elements with different
43/// sensitivities must not share a hash map. Comparing or ordering elements with
44/// different sensitivities will panic. Use [`PathElementCS`] or [`PathElementCI`]
45/// when the sensitivity is known at compile time.
46pub type PathElement<'a> = PathElementGeneric<'a, CaseSensitivity>;
47
48/// A validated, normalized single path element.
49///
50/// `PathElementGeneric` takes a raw path element name, validates it (rejecting empty
51/// strings, `.`, `..`, strings containing `/` or `\0`, and unassigned Unicode characters), normalizes it through a Unicode normalization pipeline,
52/// and computes an OS-compatible presentation form. All three views -- original,
53/// normalized, and OS-compatible -- are accessible without re-computation.
54///
55/// The type parameter `S` controls case sensitivity:
56/// - [`CaseSensitive`] -- compile-time case-sensitive (alias: [`PathElementCS`]).
57/// - [`CaseInsensitive`] -- compile-time case-insensitive (alias: [`PathElementCI`]).
58/// - [`CaseSensitivity`] -- runtime-selected (alias: [`PathElement`]).
59///
60/// Equality, ordering, and hashing are based on the **normalized** form, so two
61/// `PathElementGeneric` values with different originals but the same normalized form
62/// are considered equal. Comparing or ordering elements with different case
63/// sensitivities will panic.
64///
65/// Where possible, the normalized and OS-compatible forms borrow from the original
66/// string to avoid allocation.
67#[derive(Clone)]
68pub struct PathElementGeneric<'a, S> {
69    original: Cow<'a, str>,
70    /// Relative to `original`.
71    normalized: SubstringOrOwned,
72    /// Relative to `original`.
73    os_compatible: SubstringOrOwned,
74    case_sensitivity: S,
75}
76
77impl<S: core::fmt::Debug> core::fmt::Debug for PathElementGeneric<'_, S> {
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        f.debug_struct("PathElement")
80            .field("original", &self.original())
81            .field("normalized", &self.normalized())
82            .field("os_compatible", &self.os_compatible())
83            .field("case_sensitivity", &self.case_sensitivity)
84            .finish()
85    }
86}
87
88/// Compares by `normalized()`.
89///
90/// # Panics
91/// Panics if `self` and `other` have different [`CaseSensitivity`] values.
92/// Use [`PartialOrd`] for a non-panicking comparison that returns `None` on mismatch.
93impl<'a, S1, S2> PartialEq<PathElementGeneric<'a, S2>> for PathElementGeneric<'_, S1>
94where
95    for<'s> CaseSensitivity: From<&'s S1> + From<&'s S2>,
96{
97    fn eq(&self, other: &PathElementGeneric<'a, S2>) -> bool {
98        assert_eq!(
99            CaseSensitivity::from(&self.case_sensitivity),
100            CaseSensitivity::from(&other.case_sensitivity),
101            "comparing PathElements with different case sensitivity"
102        );
103        self.normalized() == other.normalized()
104    }
105}
106
107/// See [`PartialEq`] for panicking behavior on case sensitivity mismatch.
108impl<S> Eq for PathElementGeneric<'_, S> where for<'s> CaseSensitivity: From<&'s S> {}
109
110/// Compares by `normalized()`. Returns `None` if the two elements have
111/// different `CaseSensitivity` values.
112impl<'a, S1, S2> PartialOrd<PathElementGeneric<'a, S2>> for PathElementGeneric<'_, S1>
113where
114    for<'s> CaseSensitivity: From<&'s S1> + From<&'s S2>,
115{
116    fn partial_cmp(&self, other: &PathElementGeneric<'a, S2>) -> Option<core::cmp::Ordering> {
117        if CaseSensitivity::from(&self.case_sensitivity)
118            != CaseSensitivity::from(&other.case_sensitivity)
119        {
120            return None;
121        }
122        Some(self.normalized().cmp(other.normalized()))
123    }
124}
125
126/// Compares by `normalized()`.
127///
128/// # Panics
129/// Panics if `self` and `other` have different [`CaseSensitivity`] values.
130/// This can only happen with the runtime-dynamic [`PathElement`] type alias.
131/// The typed aliases [`PathElementCS`] and [`PathElementCI`] always have
132/// matching sensitivity, so they never panic.
133impl<S> Ord for PathElementGeneric<'_, S>
134where
135    for<'s> CaseSensitivity: From<&'s S>,
136{
137    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
138        assert_eq!(
139            CaseSensitivity::from(&self.case_sensitivity),
140            CaseSensitivity::from(&other.case_sensitivity),
141            "comparing PathElements with different case sensitivity"
142        );
143        self.normalized().cmp(other.normalized())
144    }
145}
146
147/// Hashes by `normalized()`, consistent with `PartialEq`. Only
148/// implemented for typed variants, not `PathElement`.
149impl core::hash::Hash for PathElementCS<'_> {
150    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
151        self.normalized().hash(state);
152    }
153}
154
155/// Hashes by `normalized()`, consistent with `PartialEq`. Only
156/// implemented for typed variants, not `PathElement`.
157impl core::hash::Hash for PathElementCI<'_> {
158    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
159        self.normalized().hash(state);
160    }
161}
162
163impl<'a> PathElementCS<'a> {
164    /// Creates a new case-sensitive path element from a string.
165    ///
166    /// # Errors
167    /// Returns [`Error`](crate::Error) if the name is invalid (empty, `.`, `..`,
168    /// contains `/`, `\0`, or unassigned Unicode characters).
169    ///
170    /// ```
171    /// # use normalized_path::PathElementCS;
172    /// let pe = PathElementCS::new("Hello.txt")?;
173    /// assert_eq!(pe.normalized(), "Hello.txt"); // case preserved
174    /// # Ok::<(), normalized_path::Error>(())
175    /// ```
176    pub fn new(original: impl Into<Cow<'a, str>>) -> Result<Self> {
177        Self::with_case_sensitivity(original, CaseSensitive)
178    }
179
180    /// Creates a new case-sensitive path element from a byte slice.
181    ///
182    /// # Errors
183    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
184    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
185    /// Unicode characters).
186    ///
187    /// ```
188    /// # use normalized_path::PathElementCS;
189    /// let pe = PathElementCS::from_bytes(b"hello.txt")?;
190    /// assert_eq!(pe.normalized(), "hello.txt");
191    ///
192    /// // Invalid UTF-8 is rejected
193    /// assert!(PathElementCS::from_bytes(b"hello\xff.txt").is_err());
194    /// # Ok::<(), normalized_path::Error>(())
195    /// ```
196    pub fn from_bytes(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
197        Self::from_bytes_with_case_sensitivity(original, CaseSensitive)
198    }
199
200    /// Creates a new case-sensitive path element from an OS string.
201    ///
202    /// # Errors
203    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
204    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
205    /// Unicode characters).
206    ///
207    /// ```
208    /// # use normalized_path::PathElementCS;
209    /// # use std::ffi::OsStr;
210    /// let pe = PathElementCS::from_os_str(OsStr::new("hello.txt"))?;
211    /// assert_eq!(pe.normalized(), "hello.txt");
212    /// # Ok::<(), normalized_path::Error>(())
213    /// ```
214    #[cfg(feature = "std")]
215    pub fn from_os_str(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
216        Self::from_os_str_with_case_sensitivity(original, CaseSensitive)
217    }
218}
219
220impl<'a> PathElementCI<'a> {
221    /// Creates a new case-insensitive path element from a string.
222    ///
223    /// # Errors
224    /// Returns [`Error`](crate::Error) if the name is invalid (empty, `.`, `..`,
225    /// contains `/`, `\0`, or unassigned Unicode characters).
226    ///
227    /// ```
228    /// # use normalized_path::PathElementCI;
229    /// let pe = PathElementCI::new("Hello.txt")?;
230    /// assert_eq!(pe.normalized(), "hello.txt"); // case-folded
231    /// # Ok::<(), normalized_path::Error>(())
232    /// ```
233    pub fn new(original: impl Into<Cow<'a, str>>) -> Result<Self> {
234        Self::with_case_sensitivity(original, CaseInsensitive)
235    }
236
237    /// Creates a new case-insensitive path element from a byte slice.
238    ///
239    /// # Errors
240    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
241    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
242    /// Unicode characters).
243    ///
244    /// ```
245    /// # use normalized_path::PathElementCI;
246    /// let pe = PathElementCI::from_bytes(b"Hello.TXT")?;
247    /// assert_eq!(pe.normalized(), "hello.txt");
248    /// # Ok::<(), normalized_path::Error>(())
249    /// ```
250    pub fn from_bytes(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
251        Self::from_bytes_with_case_sensitivity(original, CaseInsensitive)
252    }
253
254    /// Creates a new case-insensitive path element from an OS string.
255    ///
256    /// # Errors
257    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
258    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
259    /// Unicode characters).
260    #[cfg(feature = "std")]
261    pub fn from_os_str(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
262        Self::from_os_str_with_case_sensitivity(original, CaseInsensitive)
263    }
264}
265
266impl<'a> PathElementGeneric<'a, CaseSensitivity> {
267    /// Creates a new path element with runtime-selected case sensitivity.
268    ///
269    /// # Errors
270    /// Returns [`Error`](crate::Error) if the name is invalid (empty, `.`, `..`,
271    /// contains `/`, `\0`, or unassigned Unicode characters).
272    ///
273    /// ```
274    /// # use normalized_path::{PathElement, CaseSensitivity};
275    /// let cs = PathElement::new("Hello.txt", CaseSensitivity::Sensitive)?;
276    /// assert_eq!(cs.normalized(), "Hello.txt");
277    ///
278    /// let ci = PathElement::new("Hello.txt", CaseSensitivity::Insensitive)?;
279    /// assert_eq!(ci.normalized(), "hello.txt");
280    /// # Ok::<(), normalized_path::Error>(())
281    /// ```
282    pub fn new(
283        original: impl Into<Cow<'a, str>>,
284        case_sensitivity: impl Into<CaseSensitivity>,
285    ) -> Result<Self> {
286        Self::with_case_sensitivity(original, case_sensitivity)
287    }
288
289    /// Creates a new path element from a byte slice with runtime-selected case sensitivity.
290    ///
291    /// # Errors
292    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
293    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
294    /// Unicode characters).
295    pub fn from_bytes(
296        original: impl Into<Cow<'a, [u8]>>,
297        case_sensitivity: impl Into<CaseSensitivity>,
298    ) -> Result<Self> {
299        Self::from_bytes_with_case_sensitivity(original, case_sensitivity)
300    }
301
302    /// Creates a new path element from an OS string with runtime-selected case sensitivity.
303    ///
304    /// # Errors
305    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
306    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
307    /// Unicode characters).
308    #[cfg(feature = "std")]
309    pub fn from_os_str(
310        original: impl Into<Cow<'a, OsStr>>,
311        case_sensitivity: impl Into<CaseSensitivity>,
312    ) -> Result<Self> {
313        Self::from_os_str_with_case_sensitivity(original, case_sensitivity)
314    }
315
316    /// Convenience constructor for a case-sensitive `PathElement`.
317    ///
318    /// # Errors
319    /// Returns [`Error`](crate::Error) if the name is invalid (empty, `.`, `..`,
320    /// contains `/`, `\0`, or unassigned Unicode characters).
321    pub fn new_cs(original: impl Into<Cow<'a, str>>) -> Result<Self> {
322        Self::with_case_sensitivity(original, CaseSensitive)
323    }
324
325    /// Convenience constructor for a case-insensitive `PathElement`.
326    ///
327    /// # Errors
328    /// Returns [`Error`](crate::Error) if the name is invalid (empty, `.`, `..`,
329    /// contains `/`, `\0`, or unassigned Unicode characters).
330    pub fn new_ci(original: impl Into<Cow<'a, str>>) -> Result<Self> {
331        Self::with_case_sensitivity(original, CaseInsensitive)
332    }
333
334    /// Convenience constructor for a case-sensitive `PathElement` from bytes.
335    ///
336    /// # Errors
337    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
338    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
339    /// Unicode characters).
340    pub fn from_bytes_cs(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
341        Self::from_bytes_with_case_sensitivity(original, CaseSensitive)
342    }
343
344    /// Convenience constructor for a case-insensitive `PathElement` from bytes.
345    ///
346    /// # Errors
347    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
348    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
349    /// Unicode characters).
350    pub fn from_bytes_ci(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
351        Self::from_bytes_with_case_sensitivity(original, CaseInsensitive)
352    }
353
354    /// Convenience constructor for a case-sensitive `PathElement` from an OS string.
355    ///
356    /// # Errors
357    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
358    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
359    /// Unicode characters).
360    #[cfg(feature = "std")]
361    pub fn from_os_str_cs(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
362        Self::from_os_str_with_case_sensitivity(original, CaseSensitive)
363    }
364
365    /// Convenience constructor for a case-insensitive `PathElement` from an OS string.
366    ///
367    /// # Errors
368    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
369    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
370    /// Unicode characters).
371    #[cfg(feature = "std")]
372    pub fn from_os_str_ci(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
373        Self::from_os_str_with_case_sensitivity(original, CaseInsensitive)
374    }
375}
376
377impl<'a, S> PathElementGeneric<'a, S>
378where
379    for<'s> CaseSensitivity: From<&'s S>,
380{
381    /// Creates a new path element from a byte slice with an explicit case-sensitivity
382    /// marker.
383    ///
384    /// This is the most general byte-input constructor. The typed aliases
385    /// ([`PathElementCS::from_bytes()`], [`PathElementCI::from_bytes()`]) and the
386    /// runtime-dynamic constructors delegate to this method.
387    ///
388    /// # Errors
389    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
390    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
391    /// Unicode characters).
392    pub fn from_bytes_with_case_sensitivity(
393        original: impl Into<Cow<'a, [u8]>>,
394        case_sensitivity: impl Into<S>,
395    ) -> Result<Self> {
396        let utf8_err =
397            |bytes: &[u8]| ErrorKind::InvalidUtf8.into_error(String::from_utf8_lossy(bytes));
398        let cow_str: Cow<'a, str> = match original.into() {
399            Cow::Borrowed(b) => Cow::Borrowed(core::str::from_utf8(b).map_err(|_| utf8_err(b))?),
400            Cow::Owned(v) => Cow::Owned(String::from_utf8(v).map_err(|e| utf8_err(e.as_bytes()))?),
401        };
402        Self::with_case_sensitivity(cow_str, case_sensitivity)
403    }
404
405    /// Creates a new path element from an OS string with an explicit case-sensitivity
406    /// marker.
407    ///
408    /// # Errors
409    /// Returns [`Error`](crate::Error) if the input is not valid UTF-8 or the
410    /// name is invalid (empty, `.`, `..`, contains `/`, `\0`, or unassigned
411    /// Unicode characters).
412    #[cfg(feature = "std")]
413    pub fn from_os_str_with_case_sensitivity(
414        original: impl Into<Cow<'a, OsStr>>,
415        case_sensitivity: impl Into<S>,
416    ) -> Result<Self> {
417        let cow_bytes: Cow<'a, [u8]> = match original.into() {
418            Cow::Borrowed(os) => Cow::Borrowed(os.as_encoded_bytes()),
419            Cow::Owned(os) => Cow::Owned(os.into_encoded_bytes()),
420        };
421        Self::from_bytes_with_case_sensitivity(cow_bytes, case_sensitivity)
422    }
423
424    /// Creates a new path element with an explicit case-sensitivity marker.
425    ///
426    /// This is the most general string constructor. The typed aliases
427    /// ([`PathElementCS::new()`], [`PathElementCI::new()`]) and the runtime-dynamic
428    /// constructors delegate to this method.
429    ///
430    /// # Errors
431    /// Returns [`Error`](crate::Error) if the name is invalid (empty, `.`, `..`,
432    /// contains `/`, `\0`, or unassigned Unicode characters).
433    pub fn with_case_sensitivity(
434        original: impl Into<Cow<'a, str>>,
435        case_sensitivity: impl Into<S>,
436    ) -> Result<Self> {
437        let original = original.into();
438        let case_sensitivity = case_sensitivity.into();
439        let cs = CaseSensitivity::from(&case_sensitivity);
440
441        let cs_normalized = match normalize_cs(&original) {
442            Ok(v) => v,
443            Err(kind) => return Err(kind.into_error(original)),
444        };
445        let normalized = match cs {
446            CaseSensitivity::Sensitive => SubstringOrOwned::new(&cs_normalized, &original),
447            CaseSensitivity::Insensitive => {
448                SubstringOrOwned::new(&normalize_ci_from_normalized_cs(&cs_normalized), &original)
449            }
450        };
451        let os_str = match os_compatible_from_normalized_cs(&cs_normalized) {
452            Ok(v) => v,
453            Err(kind) => return Err(kind.into_error(original)),
454        };
455        let os_compatible = SubstringOrOwned::new(&os_str, &original);
456        Ok(Self {
457            original,
458            normalized,
459            os_compatible,
460            case_sensitivity,
461        })
462    }
463
464    /// Returns the case sensitivity of this path element as a [`CaseSensitivity`] enum.
465    ///
466    /// ```
467    /// # use normalized_path::{PathElementCS, PathElementCI, CaseSensitivity};
468    /// assert_eq!(PathElementCS::new("a")?.case_sensitivity(), CaseSensitivity::Sensitive);
469    /// assert_eq!(PathElementCI::new("a")?.case_sensitivity(), CaseSensitivity::Insensitive);
470    /// # Ok::<(), normalized_path::Error>(())
471    /// ```
472    pub fn case_sensitivity(&self) -> CaseSensitivity {
473        CaseSensitivity::from(&self.case_sensitivity)
474    }
475}
476
477impl<'a, S> PathElementGeneric<'a, S> {
478    /// Returns the original input string, before any normalization.
479    ///
480    /// ```
481    /// # use normalized_path::PathElementCS;
482    /// let pe = PathElementCS::new("  Hello.txt  ")?;
483    /// assert_eq!(pe.original(), "  Hello.txt  ");
484    /// assert_eq!(pe.normalized(), "Hello.txt");
485    /// # Ok::<(), normalized_path::Error>(())
486    /// ```
487    pub fn original(&self) -> &str {
488        &self.original
489    }
490
491    /// Consumes `self` and returns the original input string.
492    ///
493    /// ```
494    /// # use normalized_path::PathElementCS;
495    /// let pe = PathElementCS::new("  hello.txt  ")?;
496    /// assert_eq!(pe.into_original(), "  hello.txt  ");
497    /// # Ok::<(), normalized_path::Error>(())
498    /// ```
499    pub fn into_original(self) -> Cow<'a, str> {
500        self.original
501    }
502
503    /// Returns `true` if the normalized form is identical to the original.
504    ///
505    /// When this returns `true`, no allocation was needed for the normalized form.
506    ///
507    /// ```
508    /// # use normalized_path::PathElementCS;
509    /// assert!(PathElementCS::new("hello.txt")?.is_normalized());
510    /// assert!(!PathElementCS::new("  hello.txt  ")?.is_normalized());
511    /// # Ok::<(), normalized_path::Error>(())
512    /// ```
513    pub fn is_normalized(&self) -> bool {
514        self.normalized.is_identity(&self.original)
515    }
516
517    /// Returns the normalized form of the path element name.
518    ///
519    /// This is the canonical representation used for equality comparisons, ordering,
520    /// and hashing. In case-sensitive mode it is NFC-normalized; in case-insensitive
521    /// mode it is additionally case-folded.
522    ///
523    /// ```
524    /// # use normalized_path::{PathElementCS, PathElementCI};
525    /// let cs = PathElementCS::new("Caf\u{00E9}.txt")?;
526    /// assert_eq!(cs.normalized(), "Caf\u{00E9}.txt"); // case preserved
527    ///
528    /// let ci = PathElementCI::new("Caf\u{00E9}.txt")?;
529    /// assert_eq!(ci.normalized(), "caf\u{00E9}.txt"); // case-folded
530    /// # Ok::<(), normalized_path::Error>(())
531    /// ```
532    pub fn normalized(&self) -> &str {
533        self.normalized.as_ref(&self.original)
534    }
535
536    /// Consumes `self` and returns the normalized form as a [`Cow`].
537    ///
538    /// Returns `Cow::Borrowed` when the normalized form is a substring of the original
539    /// and the original was itself borrowed.
540    ///
541    /// ```
542    /// # use std::borrow::Cow;
543    /// # use normalized_path::PathElementCS;
544    /// let pe = PathElementCS::new("hello.txt")?;
545    /// assert!(matches!(pe.into_normalized(), Cow::Borrowed("hello.txt")));
546    /// # Ok::<(), normalized_path::Error>(())
547    /// ```
548    pub fn into_normalized(self) -> Cow<'a, str> {
549        self.normalized.into_cow(self.original)
550    }
551
552    /// Returns `true` if the OS-compatible form is identical to the original.
553    ///
554    /// ```
555    /// # use normalized_path::PathElementCS;
556    /// assert!(PathElementCS::new("hello.txt")?.is_os_compatible());
557    /// assert!(!PathElementCS::new("  hello.txt  ")?.is_os_compatible());
558    /// # Ok::<(), normalized_path::Error>(())
559    /// ```
560    pub fn is_os_compatible(&self) -> bool {
561        self.os_compatible.is_identity(&self.original)
562    }
563
564    /// Returns the OS-compatible presentation form of the path element name.
565    ///
566    /// ```
567    /// # use normalized_path::PathElementCS;
568    /// let pe = PathElementCS::new("hello.txt")?;
569    /// assert_eq!(pe.os_compatible(), "hello.txt");
570    /// # Ok::<(), normalized_path::Error>(())
571    /// ```
572    pub fn os_compatible(&self) -> &str {
573        self.os_compatible.as_ref(&self.original)
574    }
575
576    /// Consumes `self` and returns the OS-compatible form as a [`Cow<str>`](Cow).
577    ///
578    /// ```
579    /// # use std::borrow::Cow;
580    /// # use normalized_path::PathElementCS;
581    /// let pe = PathElementCS::new("hello.txt")?;
582    /// assert!(matches!(pe.into_os_compatible(), Cow::Borrowed("hello.txt")));
583    /// # Ok::<(), normalized_path::Error>(())
584    /// ```
585    pub fn into_os_compatible(self) -> Cow<'a, str> {
586        self.os_compatible.into_cow(self.original)
587    }
588
589    /// Returns the OS-compatible form as an [`OsStr`] reference.
590    ///
591    /// ```
592    /// # use normalized_path::PathElementCS;
593    /// # use std::ffi::OsStr;
594    /// let pe = PathElementCS::new("hello.txt")?;
595    /// assert_eq!(pe.os_str(), OsStr::new("hello.txt"));
596    /// # Ok::<(), normalized_path::Error>(())
597    /// ```
598    #[cfg(feature = "std")]
599    pub fn os_str(&self) -> &OsStr {
600        OsStr::new(self.os_compatible())
601    }
602
603    /// Consumes `self` and returns the OS-compatible form as a [`Cow<OsStr>`](Cow).
604    ///
605    /// ```
606    /// # use std::borrow::Cow;
607    /// # use std::ffi::OsStr;
608    /// # use normalized_path::PathElementCS;
609    /// let pe = PathElementCS::new("hello.txt")?;
610    /// assert!(matches!(pe.into_os_str(), Cow::Borrowed(s) if s == OsStr::new("hello.txt")));
611    /// # Ok::<(), normalized_path::Error>(())
612    /// ```
613    #[cfg(feature = "std")]
614    pub fn into_os_str(self) -> Cow<'a, OsStr> {
615        match self.into_os_compatible() {
616            Cow::Borrowed(s) => Cow::Borrowed(OsStr::new(s)),
617            Cow::Owned(s) => Cow::Owned(OsString::from(s)),
618        }
619    }
620
621    /// Returns `true` if the original string is borrowed (not owned).
622    ///
623    /// ```
624    /// # use std::borrow::Cow;
625    /// # use normalized_path::PathElementCS;
626    /// let borrowed = PathElementCS::new(Cow::Borrowed("hello"))?;
627    /// assert!(borrowed.is_borrowed());
628    ///
629    /// let owned = PathElementCS::new(Cow::<str>::Owned("hello".into()))?;
630    /// assert!(!owned.is_borrowed());
631    /// # Ok::<(), normalized_path::Error>(())
632    /// ```
633    pub fn is_borrowed(&self) -> bool {
634        matches!(self.original, Cow::Borrowed(_))
635    }
636
637    /// Returns `true` if the original string is owned (not borrowed).
638    ///
639    /// ```
640    /// # use std::borrow::Cow;
641    /// # use normalized_path::PathElementCS;
642    /// let owned = PathElementCS::new(Cow::<str>::Owned("hello".into()))?;
643    /// assert!(owned.is_owned());
644    ///
645    /// let borrowed = PathElementCS::new("hello")?;
646    /// assert!(!borrowed.is_owned());
647    /// # Ok::<(), normalized_path::Error>(())
648    /// ```
649    pub fn is_owned(&self) -> bool {
650        matches!(self.original, Cow::Owned(_))
651    }
652
653    /// Consumes `self` and returns an equivalent `PathElementGeneric` with a `'static`
654    /// lifetime by cloning the original string if it was borrowed.
655    ///
656    /// ```
657    /// # use normalized_path::PathElementCS;
658    /// let pe = PathElementCS::new("hello.txt")?;
659    /// let owned: PathElementCS<'static> = pe.into_owned();
660    /// assert!(owned.is_owned());
661    /// assert_eq!(owned.normalized(), "hello.txt");
662    /// # Ok::<(), normalized_path::Error>(())
663    /// ```
664    pub fn into_owned(self) -> PathElementGeneric<'static, S> {
665        PathElementGeneric {
666            original: Cow::Owned(self.original.into_owned()),
667            normalized: self.normalized,
668            os_compatible: self.os_compatible,
669            case_sensitivity: self.case_sensitivity,
670        }
671    }
672}
673
674// --- Conversions ---
675
676/// Converts a compile-time case-sensitive element into a runtime-dynamic [`PathElement`].
677impl<'a> From<PathElementCS<'a>> for PathElement<'a> {
678    fn from(pe: PathElementCS<'a>) -> Self {
679        Self {
680            original: pe.original,
681            normalized: pe.normalized,
682            os_compatible: pe.os_compatible,
683            case_sensitivity: CaseSensitivity::Sensitive,
684        }
685    }
686}
687
688/// Converts a compile-time case-insensitive element into a runtime-dynamic [`PathElement`].
689impl<'a> From<PathElementCI<'a>> for PathElement<'a> {
690    fn from(pe: PathElementCI<'a>) -> Self {
691        Self {
692            original: pe.original,
693            normalized: pe.normalized,
694            os_compatible: pe.os_compatible,
695            case_sensitivity: CaseSensitivity::Insensitive,
696        }
697    }
698}
699
700/// Attempts to convert a runtime-dynamic [`PathElement`] into a [`PathElementCS`].
701///
702/// Succeeds if the element is case-sensitive. On failure, returns the element
703/// re-wrapped as a [`PathElementCI`] in the `Err` variant (no data is lost).
704impl<'a> TryFrom<PathElement<'a>> for PathElementCS<'a> {
705    type Error = PathElementCI<'a>;
706
707    fn try_from(pe: PathElement<'a>) -> core::result::Result<Self, Self::Error> {
708        if pe.case_sensitivity == CaseSensitivity::Sensitive {
709            Ok(Self {
710                original: pe.original,
711                normalized: pe.normalized,
712                os_compatible: pe.os_compatible,
713                case_sensitivity: CaseSensitive,
714            })
715        } else {
716            Err(Self::Error {
717                original: pe.original,
718                normalized: pe.normalized,
719                os_compatible: pe.os_compatible,
720                case_sensitivity: CaseInsensitive,
721            })
722        }
723    }
724}
725
726/// Attempts to convert a runtime-dynamic [`PathElement`] into a [`PathElementCI`].
727///
728/// Succeeds if the element is case-insensitive. On failure, returns the element
729/// re-wrapped as a [`PathElementCS`] in the `Err` variant (no data is lost).
730impl<'a> TryFrom<PathElement<'a>> for PathElementCI<'a> {
731    type Error = PathElementCS<'a>;
732
733    fn try_from(pe: PathElement<'a>) -> core::result::Result<Self, Self::Error> {
734        if pe.case_sensitivity == CaseSensitivity::Insensitive {
735            Ok(Self {
736                original: pe.original,
737                normalized: pe.normalized,
738                os_compatible: pe.os_compatible,
739                case_sensitivity: CaseInsensitive,
740            })
741        } else {
742            Err(Self::Error {
743                original: pe.original,
744                normalized: pe.normalized,
745                os_compatible: pe.os_compatible,
746                case_sensitivity: CaseSensitive,
747            })
748        }
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use alloc::borrow::Cow;
755    use alloc::string::ToString;
756    use alloc::vec;
757    use alloc::vec::Vec;
758
759    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
760    use wasm_bindgen_test::wasm_bindgen_test as test;
761
762    use super::{PathElement, PathElementCI, PathElementCS};
763    use crate::ErrorKind;
764    use crate::case_sensitivity::{CaseInsensitive, CaseSensitive, CaseSensitivity};
765    use crate::normalize::{normalize_ci_from_normalized_cs, normalize_cs};
766    use crate::os::os_compatible_from_normalized_cs;
767
768    // --- PathElement ---
769
770    // CS "H\tllo": original="H\tllo", normalized="H␉llo", os_compatible="H␉llo"
771    // original != normalized (control mapping), os_compatible == normalized on all platforms.
772    #[test]
773    fn path_element_cs_matches_freestanding() {
774        let input = "H\tllo";
775        let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
776        assert_eq!(pe.original(), input);
777        assert_eq!(pe.normalized(), normalize_cs(input).unwrap().as_ref());
778        assert_eq!(
779            pe.os_compatible(),
780            os_compatible_from_normalized_cs(&normalize_cs(input).unwrap())
781                .unwrap()
782                .as_ref()
783        );
784    }
785
786    // CI "H\tllo": original="H\tllo", os_compatible="H␉llo", normalized="h␉llo"
787    // All three differ on all platforms.
788    #[test]
789    fn path_element_ci_matches_freestanding() {
790        let input = "H\tllo";
791        let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
792        assert_eq!(pe.original(), "H\tllo");
793        assert_eq!(pe.normalized(), "h\u{2409}llo");
794        assert_eq!(pe.os_compatible(), "H\u{2409}llo");
795    }
796
797    // CS "nul.e\u{0301}": normalized="nul.é" (NFC), os_compatible is platform-dependent.
798    // Windows: "\u{FF4E}ul.é" (reserved name), Apple: "nul.e\u{0301}" (NFD), Linux: "nul.é".
799    #[test]
800    fn path_element_cs_os_compatible_platform_dependent() {
801        let input = "nul.e\u{0301}";
802        let pe = PathElementCS::new(input).unwrap();
803        assert_eq!(pe.original(), "nul.e\u{0301}");
804        assert_eq!(pe.normalized(), "nul.\u{00E9}");
805        #[cfg(target_os = "windows")]
806        assert_eq!(pe.os_compatible(), "\u{FF4E}ul.\u{00E9}");
807        #[cfg(target_vendor = "apple")]
808        assert_eq!(pe.os_compatible(), "nul.e\u{0301}");
809        #[cfg(not(any(target_os = "windows", target_vendor = "apple")))]
810        assert_eq!(pe.os_compatible(), "nul.\u{00E9}");
811    }
812
813    #[test]
814    fn path_element_cs_nfc_matches_freestanding() {
815        let input = "e\u{0301}.txt";
816        let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
817        assert_eq!(pe.normalized(), normalize_cs(input).unwrap().as_ref());
818    }
819
820    #[test]
821    fn path_element_ci_casefold_matches_freestanding() {
822        let input = "Hello.txt";
823        let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
824        let cs = normalize_cs(input).unwrap();
825        assert_eq!(
826            pe.normalized(),
827            normalize_ci_from_normalized_cs(&cs).as_ref()
828        );
829    }
830
831    #[test]
832    fn path_element_cs_normalized_borrows_from_original() {
833        let input = "hello.txt";
834        let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
835        assert_eq!(pe.normalized(), "hello.txt");
836        assert!(core::ptr::eq(pe.normalized().as_ptr(), input.as_ptr()));
837    }
838
839    #[test]
840    fn path_element_cs_into_normalized_borrows() {
841        let input = "hello.txt";
842        let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
843        let norm = pe.into_normalized();
844        assert!(matches!(norm, Cow::Borrowed(_)));
845        assert_eq!(norm, "hello.txt");
846    }
847
848    #[test]
849    fn path_element_cs_into_os_compatible_borrows() {
850        let input = "hello.txt";
851        let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
852        let pres = pe.into_os_compatible();
853        assert!(matches!(pres, Cow::Borrowed(_)));
854        assert_eq!(pres.as_ref(), "hello.txt");
855    }
856
857    #[test]
858    fn path_element_ci_normalized_borrows_when_already_folded() {
859        let input = "hello.txt";
860        let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
861        assert!(core::ptr::eq(pe.normalized().as_ptr(), input.as_ptr()));
862    }
863
864    #[test]
865    fn path_element_ci_into_normalized_borrows_when_already_folded() {
866        let input = "hello.txt";
867        let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
868        let norm = pe.into_normalized();
869        assert!(matches!(norm, Cow::Borrowed(_)));
870        assert_eq!(norm, "hello.txt");
871    }
872
873    #[test]
874    fn path_element_ci_into_os_compatible_borrows_when_already_folded() {
875        let input = "hello.txt";
876        let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
877        let pres = pe.into_os_compatible();
878        assert!(matches!(pres, Cow::Borrowed(_)));
879        assert_eq!(pres.as_ref(), "hello.txt");
880    }
881
882    #[test]
883    fn path_element_cs_trimmed_borrows_suffix() {
884        let input = "   hello.txt";
885        let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
886        assert_eq!(pe.normalized(), "hello.txt");
887        assert!(core::ptr::eq(pe.normalized().as_ptr(), input[3..].as_ptr()));
888    }
889
890    #[test]
891    fn path_element_into_original_returns_original() {
892        let input = "  Hello.txt  ";
893        let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
894        let orig = pe.into_original();
895        assert!(matches!(orig, Cow::Borrowed(_)));
896        assert_eq!(orig, input);
897    }
898
899    #[test]
900    fn path_element_is_normalized_when_unchanged() {
901        let pe = PathElementCS::new("hello.txt").unwrap();
902        assert!(pe.is_normalized());
903    }
904
905    #[test]
906    fn path_element_is_not_normalized_when_trimmed() {
907        let pe = PathElementCS::new("  hello.txt  ").unwrap();
908        assert!(!pe.is_normalized());
909    }
910
911    #[test]
912    fn path_element_is_not_normalized_when_trailing_whitespace_trimmed() {
913        // Trailing-only trim produces Substring(0, shorter_len) — a prefix, not identity.
914        let pe = PathElementCS::new("hello.txt  ").unwrap();
915        assert!(!pe.is_normalized());
916        assert_eq!(pe.normalized(), "hello.txt");
917    }
918
919    #[test]
920    fn path_element_is_not_normalized_when_casefolded() {
921        let pe = PathElementCI::new("Hello.txt").unwrap();
922        assert!(!pe.is_normalized());
923    }
924
925    #[test]
926    fn path_element_ci_is_normalized_when_already_folded() {
927        let pe = PathElementCI::new("hello.txt").unwrap();
928        assert!(pe.is_normalized());
929    }
930
931    #[test]
932    fn path_element_is_os_compatible_ascii() {
933        let pe = PathElementCS::new("hello.txt").unwrap();
934        assert!(pe.is_os_compatible());
935    }
936
937    #[test]
938    fn path_element_is_not_os_compatible_trailing_whitespace_ci() {
939        // CI: os_compatible is relative to original. Trailing trim produces
940        // Substring(0, shorter_len) — a prefix, not identity.
941        let pe = PathElementCI::new("hello.txt  ").unwrap();
942        assert!(!pe.is_os_compatible());
943    }
944
945    #[test]
946    fn path_element_is_not_os_compatible_trailing_whitespace_cs() {
947        // CS: both normalized and os_compatible must be identity.
948        // Trailing trim makes normalized a prefix of original.
949        let pe = PathElementCS::new("hello.txt  ").unwrap();
950        assert!(!pe.is_os_compatible());
951    }
952
953    #[test]
954    fn path_element_is_os_compatible_nfc_input() {
955        // NFC input "é" stays NFC after normalization. On Apple, os_compatible
956        // converts to NFD "e\u{0301}", so original != os_compatible.
957        // On non-Apple, os_compatible == normalized == original.
958        let pe = PathElementCS::new("\u{00E9}.txt").unwrap();
959        #[cfg(target_vendor = "apple")]
960        assert!(!pe.is_os_compatible());
961        #[cfg(not(target_vendor = "apple"))]
962        assert!(pe.is_os_compatible());
963    }
964
965    #[test]
966    fn path_element_is_os_compatible_nfd_input() {
967        // NFD input "e\u{0301}" normalizes to NFC "é", so original != os_compatible
968        // on non-Apple. On Apple, os_compatible converts back to NFD, matching the original.
969        let pe = PathElementCS::new("e\u{0301}.txt").unwrap();
970        #[cfg(target_vendor = "apple")]
971        assert!(pe.is_os_compatible());
972        #[cfg(not(target_vendor = "apple"))]
973        assert!(!pe.is_os_compatible());
974    }
975
976    #[test]
977    fn path_element_is_os_compatible_nfd_input_ci() {
978        // Regression: NFD input in CI mode. cs_normalized is NFC (owned allocation),
979        // then os_compatible_from_normalized_cs may convert back to NFD. The result string-equals
980        // original but is stored as Owned (pointer doesn't overlap original).
981        // The identity fast-path misses this; string comparison fallback catches it.
982        let pe = PathElementCI::new("e\u{0301}.txt").unwrap();
983        #[cfg(target_vendor = "apple")]
984        assert!(pe.is_os_compatible());
985        #[cfg(not(target_vendor = "apple"))]
986        assert!(!pe.is_os_compatible());
987    }
988
989    #[test]
990    fn path_element_is_os_compatible_nfc_input_ci() {
991        // NFC input in CI mode. cs_normalized borrows from original (already NFC).
992        // On Apple, os_compatible converts to NFD, so original != os_compatible.
993        let pe = PathElementCI::new("\u{00E9}.txt").unwrap();
994        #[cfg(target_vendor = "apple")]
995        assert!(!pe.is_os_compatible());
996        #[cfg(not(target_vendor = "apple"))]
997        assert!(pe.is_os_compatible());
998    }
999
1000    #[test]
1001    fn path_element_is_not_os_compatible_reserved_on_windows() {
1002        let pe = PathElementCS::new("nul.txt").unwrap();
1003        #[cfg(target_os = "windows")]
1004        assert!(!pe.is_os_compatible());
1005        #[cfg(not(target_os = "windows"))]
1006        assert!(pe.is_os_compatible());
1007    }
1008
1009    #[test]
1010    fn path_element_borrowed_is_borrowed() {
1011        let pe = PathElementCS::new(Cow::Borrowed("hello.txt")).unwrap();
1012        assert!(pe.is_borrowed());
1013        assert!(!pe.is_owned());
1014    }
1015
1016    #[test]
1017    fn path_element_owned_is_owned() {
1018        let pe = PathElementCS::new(Cow::Owned("hello.txt".to_string())).unwrap();
1019        assert!(pe.is_owned());
1020        assert!(!pe.is_borrowed());
1021    }
1022
1023    #[test]
1024    fn path_element_into_owned_is_owned() {
1025        let pe = PathElementCS::new(Cow::Borrowed("hello.txt")).unwrap();
1026        let owned = pe.into_owned();
1027        assert!(owned.is_owned());
1028    }
1029
1030    #[test]
1031    fn path_element_into_owned_preserves_values() {
1032        let input = "H\tllo";
1033        let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
1034        let owned = pe.into_owned();
1035        assert_eq!(owned.original(), "H\tllo");
1036        assert_eq!(owned.normalized(), "h\u{2409}llo");
1037        assert_eq!(owned.os_compatible(), "H\u{2409}llo");
1038    }
1039
1040    #[test]
1041    fn new_rejects_empty() {
1042        assert_eq!(PathElementCS::new("").unwrap_err().kind(), ErrorKind::Empty);
1043    }
1044
1045    #[test]
1046    fn new_rejects_dot() {
1047        assert_eq!(
1048            PathElementCS::new(".").unwrap_err().kind(),
1049            ErrorKind::CurrentDirectoryMarker
1050        );
1051    }
1052
1053    #[test]
1054    fn new_rejects_dotdot() {
1055        assert_eq!(
1056            PathElementCS::new("..").unwrap_err().kind(),
1057            ErrorKind::ParentDirectoryMarker
1058        );
1059    }
1060
1061    #[test]
1062    fn new_rejects_slash() {
1063        assert_eq!(
1064            PathElementCS::new("a/b").unwrap_err().kind(),
1065            ErrorKind::ContainsForwardSlash
1066        );
1067    }
1068
1069    #[test]
1070    fn new_rejects_null() {
1071        assert_eq!(
1072            PathElementCS::new("\0").unwrap_err().kind(),
1073            ErrorKind::ContainsNullByte
1074        );
1075    }
1076
1077    #[test]
1078    fn new_rejects_unassigned() {
1079        assert_eq!(
1080            PathElementCS::new("\u{0378}").unwrap_err().kind(),
1081            ErrorKind::ContainsUnassignedChar
1082        );
1083    }
1084
1085    // --- PartialEq / Eq ---
1086
1087    #[test]
1088    fn path_element_eq_same_cs() {
1089        let a = PathElementCS::new("hello.txt").unwrap();
1090        let b = PathElementCS::new("hello.txt").unwrap();
1091        assert_eq!(a, b);
1092    }
1093
1094    #[test]
1095    fn path_element_eq_different_original_same_normalized_cs() {
1096        let a = PathElementCS::new("  hello.txt  ").unwrap();
1097        let b = PathElementCS::new("hello.txt").unwrap();
1098        assert_ne!(a.original(), b.original());
1099        assert_eq!(a, b);
1100    }
1101
1102    #[test]
1103    fn path_element_ne_different_case_cs() {
1104        let a = PathElementCS::new("Hello.txt").unwrap();
1105        let b = PathElementCS::new("hello.txt").unwrap();
1106        assert_ne!(a, b);
1107    }
1108
1109    #[test]
1110    fn path_element_eq_different_case_ci() {
1111        let a = PathElementCI::new("Hello.txt").unwrap();
1112        let b = PathElementCI::new("hello.txt").unwrap();
1113        assert_eq!(a, b);
1114    }
1115
1116    #[test]
1117    fn path_element_eq_nfc_nfd_cs() {
1118        let a = PathElementCS::new("\u{00E9}.txt").unwrap();
1119        let b = PathElementCS::new("e\u{0301}.txt").unwrap();
1120        assert_eq!(a, b);
1121    }
1122
1123    #[test]
1124    fn path_element_eq_cross_lifetime() {
1125        let owned = PathElementCS::new("hello.txt").unwrap().into_owned();
1126        let input = "hello.txt";
1127        let borrowed = PathElementCS::new(Cow::Borrowed(input)).unwrap();
1128        assert_eq!(owned, borrowed);
1129        assert_eq!(borrowed, owned);
1130    }
1131
1132    #[test]
1133    #[should_panic(expected = "different case sensitivity")]
1134    fn path_element_eq_panics_on_mixed_dynamic_case_sensitivity() {
1135        let a = PathElement::new("hello", CaseSensitive).unwrap();
1136        let b = PathElement::new("hello", CaseInsensitive).unwrap();
1137        let _ = a == b;
1138    }
1139
1140    // --- PartialOrd / Ord ---
1141
1142    #[test]
1143    fn path_element_ord_alphabetical_cs() {
1144        let a = PathElementCS::new("apple").unwrap();
1145        let b = PathElementCS::new("banana").unwrap();
1146        assert!(a < b);
1147        assert!(b > a);
1148    }
1149
1150    #[test]
1151    fn path_element_ord_equal_cs() {
1152        let a = PathElementCS::new("hello").unwrap();
1153        let b = PathElementCS::new("hello").unwrap();
1154        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
1155    }
1156
1157    #[test]
1158    fn path_element_ord_case_ci() {
1159        let a = PathElementCI::new("Apple").unwrap();
1160        let b = PathElementCI::new("apple").unwrap();
1161        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
1162    }
1163
1164    #[test]
1165    fn path_element_partial_ord_cross_lifetime() {
1166        let owned = PathElementCS::new("apple").unwrap().into_owned();
1167        let input = "banana";
1168        let borrowed = PathElementCS::new(Cow::Borrowed(input)).unwrap();
1169        assert!(owned < borrowed);
1170    }
1171
1172    #[test]
1173    fn path_element_partial_ord_none_on_mixed_dynamic_case_sensitivity() {
1174        let a = PathElement::new("hello", CaseSensitive).unwrap();
1175        let b = PathElement::new("hello", CaseInsensitive).unwrap();
1176        assert_eq!(a.partial_cmp(&b), None);
1177    }
1178
1179    #[test]
1180    fn path_element_ord_sortable() {
1181        let mut elems: Vec<_> = ["cherry", "apple", "banana"]
1182            .iter()
1183            .map(|s| PathElementCS::new(Cow::Borrowed(*s)).unwrap())
1184            .collect();
1185        elems.sort();
1186        let names: Vec<_> = elems.iter().map(PathElementCS::normalized).collect();
1187        assert_eq!(names, &["apple", "banana", "cherry"]);
1188    }
1189
1190    #[test]
1191    fn path_element_ord_ci_sortable() {
1192        let mut elems: Vec<_> = ["Cherry", "apple", "BANANA"]
1193            .iter()
1194            .map(|s| PathElementCI::new(Cow::Borrowed(*s)).unwrap())
1195            .collect();
1196        elems.sort();
1197        let names: Vec<_> = elems.iter().map(PathElementCI::normalized).collect();
1198        assert_eq!(names, &["apple", "banana", "cherry"]);
1199    }
1200
1201    // --- Conversions ---
1202
1203    #[test]
1204    fn from_cs_into_dynamic() {
1205        let pe = PathElementCS::new("hello").unwrap();
1206        let dyn_pe: PathElement<'_> = pe.into();
1207        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
1208        assert_eq!(dyn_pe.normalized(), "hello");
1209    }
1210
1211    #[test]
1212    fn from_ci_into_dynamic() {
1213        let pe = PathElementCI::new("Hello").unwrap();
1214        let dyn_pe: PathElement<'_> = pe.into();
1215        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
1216        assert_eq!(dyn_pe.normalized(), "hello");
1217    }
1218
1219    #[test]
1220    fn try_from_dynamic_to_cs() {
1221        let pe = PathElement::new("hello", CaseSensitive).unwrap();
1222        let cs_pe: PathElementCS<'_> = pe.try_into().unwrap();
1223        assert_eq!(cs_pe.normalized(), "hello");
1224    }
1225
1226    #[test]
1227    fn try_from_dynamic_to_cs_wrong_variant() {
1228        let pe = PathElement::new("Hello", CaseInsensitive).unwrap();
1229        let err: PathElementCI<'_> = PathElementCS::try_from(pe).unwrap_err();
1230        assert_eq!(err.original(), "Hello");
1231        assert_eq!(err.normalized(), "hello");
1232        assert_eq!(err.os_compatible(), "Hello");
1233    }
1234
1235    #[test]
1236    fn try_from_dynamic_to_ci() {
1237        let pe = PathElement::new("Hello", CaseInsensitive).unwrap();
1238        let ci_pe: PathElementCI<'_> = pe.try_into().unwrap();
1239        assert_eq!(ci_pe.normalized(), "hello");
1240    }
1241
1242    // --- PathElement convenience constructors ---
1243
1244    #[test]
1245    fn dyn_new_cs() {
1246        let pe = PathElement::new_cs("Hello.txt").unwrap();
1247        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
1248        assert_eq!(pe.normalized(), "Hello.txt");
1249    }
1250
1251    #[test]
1252    fn dyn_new_ci() {
1253        let pe = PathElement::new_ci("Hello.txt").unwrap();
1254        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
1255        assert_eq!(pe.normalized(), "hello.txt");
1256    }
1257
1258    #[test]
1259    fn dyn_new_cs_matches_typed() {
1260        let dyn_pe = PathElement::new_cs("Hello.txt").unwrap();
1261        let cs_pe = PathElementCS::new("Hello.txt").unwrap();
1262        assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
1263        assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
1264    }
1265
1266    #[test]
1267    fn dyn_new_ci_matches_typed() {
1268        let dyn_pe = PathElement::new_ci("Hello.txt").unwrap();
1269        let ci_pe = PathElementCI::new("Hello.txt").unwrap();
1270        assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
1271        assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
1272    }
1273
1274    // --- case_sensitivity() getter ---
1275
1276    #[test]
1277    fn case_sensitivity_cs() {
1278        let pe = PathElementCS::new("hello").unwrap();
1279        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
1280    }
1281
1282    #[test]
1283    fn case_sensitivity_ci() {
1284        let pe = PathElementCI::new("hello").unwrap();
1285        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
1286    }
1287
1288    #[test]
1289    fn case_sensitivity_dyn() {
1290        let cs = PathElement::new("hello", CaseSensitive).unwrap();
1291        let ci = PathElement::new("hello", CaseInsensitive).unwrap();
1292        assert_eq!(cs.case_sensitivity(), CaseSensitivity::Sensitive);
1293        assert_eq!(ci.case_sensitivity(), CaseSensitivity::Insensitive);
1294    }
1295
1296    // --- PartialOrd returns None on mismatch ---
1297
1298    #[test]
1299    fn partial_ord_dyn_same_case_sensitivity() {
1300        let a = PathElement::new("apple", CaseSensitive).unwrap();
1301        let b = PathElement::new("banana", CaseSensitive).unwrap();
1302        assert!(a < b);
1303    }
1304
1305    #[test]
1306    fn partial_ord_dyn_none_on_mismatch() {
1307        let a = PathElement::new("hello", CaseSensitive).unwrap();
1308        let b = PathElement::new("hello", CaseInsensitive).unwrap();
1309        assert_eq!(a.partial_cmp(&b), None);
1310    }
1311
1312    // --- TryFrom error returns original ---
1313
1314    #[test]
1315    fn try_from_dynamic_to_ci_wrong_variant() {
1316        let pe = PathElement::new("Hello", CaseSensitive).unwrap();
1317        let err: PathElementCS<'_> = PathElementCI::try_from(pe).unwrap_err();
1318        assert_eq!(err.original(), "Hello");
1319        assert_eq!(err.normalized(), "Hello");
1320        assert_eq!(err.os_compatible(), "Hello");
1321    }
1322
1323    // --- into_owned preserves case_sensitivity ---
1324
1325    #[test]
1326    fn into_owned_preserves_cs_case_sensitivity() {
1327        let pe = PathElementCS::new("hello").unwrap();
1328        let owned = pe.into_owned();
1329        assert_eq!(owned.case_sensitivity(), CaseSensitivity::Sensitive);
1330    }
1331
1332    #[test]
1333    fn into_owned_preserves_dyn_case_sensitivity() {
1334        let pe = PathElement::new("hello", CaseInsensitive).unwrap();
1335        let owned = pe.into_owned();
1336        assert_eq!(owned.case_sensitivity(), CaseSensitivity::Insensitive);
1337    }
1338
1339    // --- Cross-type PartialEq ---
1340
1341    #[test]
1342    fn eq_cs_vs_dyn_same_case_sensitivity() {
1343        let cs = PathElementCS::new("hello").unwrap();
1344        let dyn_cs = PathElement::new_cs("hello").unwrap();
1345        assert_eq!(cs, dyn_cs);
1346        assert_eq!(dyn_cs, cs);
1347    }
1348
1349    #[test]
1350    fn eq_ci_vs_dyn_same_case_sensitivity() {
1351        let ci = PathElementCI::new("Hello").unwrap();
1352        let dyn_ci = PathElement::new_ci("hello").unwrap();
1353        assert_eq!(ci, dyn_ci);
1354        assert_eq!(dyn_ci, ci);
1355    }
1356
1357    #[test]
1358    #[should_panic(expected = "different case sensitivity")]
1359    fn eq_cs_vs_ci_panics() {
1360        let cs = PathElementCS::new("hello").unwrap();
1361        let ci = PathElementCI::new("hello").unwrap();
1362        let _ = cs == ci;
1363    }
1364
1365    #[test]
1366    #[should_panic(expected = "different case sensitivity")]
1367    fn eq_cs_vs_dyn_ci_panics() {
1368        let cs = PathElementCS::new("hello").unwrap();
1369        let dyn_ci = PathElement::new_ci("hello").unwrap();
1370        let _ = cs == dyn_ci;
1371    }
1372
1373    // --- Cross-type PartialOrd ---
1374
1375    #[test]
1376    fn partial_ord_cs_vs_dyn_same_case_sensitivity() {
1377        let cs = PathElementCS::new("apple").unwrap();
1378        let dyn_cs = PathElement::new_cs("banana").unwrap();
1379        assert!(cs < dyn_cs);
1380        assert!(dyn_cs > cs);
1381    }
1382
1383    #[test]
1384    fn partial_ord_cs_vs_ci_none() {
1385        let cs = PathElementCS::new("hello").unwrap();
1386        let ci = PathElementCI::new("hello").unwrap();
1387        assert_eq!(cs.partial_cmp(&ci), None);
1388        assert_eq!(ci.partial_cmp(&cs), None);
1389    }
1390
1391    #[test]
1392    fn partial_ord_cs_vs_dyn_ci_none() {
1393        let cs = PathElementCS::new("hello").unwrap();
1394        let dyn_ci = PathElement::new_ci("hello").unwrap();
1395        assert_eq!(cs.partial_cmp(&dyn_ci), None);
1396        assert_eq!(dyn_ci.partial_cmp(&cs), None);
1397    }
1398
1399    // --- from_bytes ---
1400
1401    #[test]
1402    fn from_bytes_cs_borrowed_matches_new() {
1403        let pe_bytes = PathElementCS::from_bytes(b"hello.txt" as &[u8]).unwrap();
1404        let pe_str = PathElementCS::new("hello.txt").unwrap();
1405        assert_eq!(pe_bytes.original(), pe_str.original());
1406        assert_eq!(pe_bytes.normalized(), pe_str.normalized());
1407        assert_eq!(pe_bytes.os_compatible(), pe_str.os_compatible());
1408    }
1409
1410    #[test]
1411    fn from_bytes_ci_borrowed_matches_new() {
1412        let pe_bytes = PathElementCI::from_bytes(b"Hello.txt" as &[u8]).unwrap();
1413        let pe_str = PathElementCI::new("Hello.txt").unwrap();
1414        assert_eq!(pe_bytes.original(), pe_str.original());
1415        assert_eq!(pe_bytes.normalized(), pe_str.normalized());
1416        assert_eq!(pe_bytes.os_compatible(), pe_str.os_compatible());
1417    }
1418
1419    #[test]
1420    fn from_bytes_owned_matches_new() {
1421        let pe_bytes = PathElementCS::from_bytes(b"hello.txt".to_vec()).unwrap();
1422        let pe_str = PathElementCS::new("hello.txt").unwrap();
1423        assert_eq!(pe_bytes.original(), pe_str.original());
1424        assert_eq!(pe_bytes.normalized(), pe_str.normalized());
1425    }
1426
1427    #[test]
1428    fn from_bytes_borrowed_preserves_borrow() {
1429        let input: &[u8] = b"hello.txt";
1430        let pe = PathElementCS::from_bytes(input).unwrap();
1431        let orig = pe.into_original();
1432        assert!(matches!(orig, Cow::Borrowed(_)));
1433    }
1434
1435    #[test]
1436    fn from_bytes_owned_is_owned() {
1437        let pe = PathElementCS::from_bytes(b"hello.txt".to_vec()).unwrap();
1438        assert!(pe.is_owned());
1439    }
1440
1441    #[test]
1442    fn from_bytes_invalid_utf8_borrowed_rejected() {
1443        let input: &[u8] = &[0x68, 0x69, 0xFF]; // "hi" + invalid byte
1444        let err = PathElementCS::from_bytes(input).unwrap_err();
1445        assert_eq!(err.kind(), ErrorKind::InvalidUtf8);
1446    }
1447
1448    #[test]
1449    fn from_bytes_invalid_utf8_owned_rejected() {
1450        let input = vec![0x68, 0x69, 0xFF];
1451        let err = PathElementCS::from_bytes(input).unwrap_err();
1452        assert_eq!(err.kind(), ErrorKind::InvalidUtf8);
1453    }
1454
1455    #[test]
1456    fn from_bytes_dynamic_case_sensitivity() {
1457        let pe = PathElement::from_bytes(b"Hello.txt" as &[u8], CaseInsensitive).unwrap();
1458        assert_eq!(pe.normalized(), "hello.txt");
1459        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
1460    }
1461
1462    #[test]
1463    fn from_bytes_cs_matches_typed() {
1464        let input: &[u8] = b"Hello.txt";
1465        let dyn_pe = PathElement::from_bytes_cs(input).unwrap();
1466        let cs_pe = PathElementCS::from_bytes(input).unwrap();
1467        assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
1468        assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
1469        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
1470    }
1471
1472    #[test]
1473    fn from_bytes_ci_matches_typed() {
1474        let input: &[u8] = b"Hello.txt";
1475        let dyn_pe = PathElement::from_bytes_ci(input).unwrap();
1476        let ci_pe = PathElementCI::from_bytes(input).unwrap();
1477        assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
1478        assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
1479        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
1480    }
1481
1482    #[test]
1483    fn from_bytes_with_case_sensitivity_cs() {
1484        let pe = PathElementCS::from_bytes(b"Hello.txt" as &[u8]).unwrap();
1485        assert_eq!(pe.normalized(), "Hello.txt");
1486    }
1487
1488    #[test]
1489    fn from_bytes_with_case_sensitivity_ci() {
1490        let pe = PathElementCI::from_bytes(b"Hello.txt" as &[u8]).unwrap();
1491        assert_eq!(pe.normalized(), "hello.txt");
1492    }
1493
1494    #[test]
1495    fn from_bytes_rejects_empty() {
1496        assert_eq!(
1497            PathElementCS::from_bytes(b"" as &[u8]).unwrap_err().kind(),
1498            ErrorKind::Empty
1499        );
1500    }
1501
1502    #[test]
1503    fn from_bytes_rejects_dot() {
1504        assert_eq!(
1505            PathElementCS::from_bytes(b"." as &[u8]).unwrap_err().kind(),
1506            ErrorKind::CurrentDirectoryMarker
1507        );
1508    }
1509
1510    #[test]
1511    fn from_bytes_rejects_dotdot() {
1512        assert_eq!(
1513            PathElementCS::from_bytes(b".." as &[u8])
1514                .unwrap_err()
1515                .kind(),
1516            ErrorKind::ParentDirectoryMarker
1517        );
1518    }
1519
1520    #[test]
1521    fn from_bytes_rejects_slash() {
1522        assert_eq!(
1523            PathElementCS::from_bytes(b"a/b" as &[u8])
1524                .unwrap_err()
1525                .kind(),
1526            ErrorKind::ContainsForwardSlash
1527        );
1528    }
1529
1530    #[test]
1531    fn from_bytes_rejects_null() {
1532        assert_eq!(
1533            PathElementCS::from_bytes(b"\0" as &[u8])
1534                .unwrap_err()
1535                .kind(),
1536            ErrorKind::ContainsNullByte
1537        );
1538    }
1539
1540    #[test]
1541    fn from_bytes_rejects_unassigned() {
1542        assert_eq!(
1543            PathElementCS::from_bytes("\u{0378}".as_bytes())
1544                .unwrap_err()
1545                .kind(),
1546            ErrorKind::ContainsUnassignedChar
1547        );
1548    }
1549
1550    #[test]
1551    fn from_bytes_dynamic_sensitive() {
1552        let pe = PathElement::from_bytes(b"Hello.txt" as &[u8], CaseSensitive).unwrap();
1553        assert_eq!(pe.normalized(), "Hello.txt");
1554        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
1555    }
1556
1557    // --- Invalid UTF-8 rejection ---
1558
1559    #[test]
1560    fn from_bytes_overlong_null() {
1561        // 0xC0 0x80 is an overlong encoding — rejected.
1562        let input: &[u8] = &[0x61, 0xC0, 0x80, 0x62];
1563        assert_eq!(
1564            PathElementCS::from_bytes(input).unwrap_err().kind(),
1565            ErrorKind::InvalidUtf8
1566        );
1567    }
1568
1569    #[test]
1570    fn from_bytes_surrogate_bytes_rejected() {
1571        let input: &[u8] = &[0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80];
1572        assert_eq!(
1573            PathElementCS::from_bytes(input).unwrap_err().kind(),
1574            ErrorKind::InvalidUtf8
1575        );
1576    }
1577
1578    #[test]
1579    fn from_bytes_lone_high_surrogate_rejected() {
1580        let input: &[u8] = &[0x61, 0xED, 0xA0, 0x80, 0x62];
1581        assert_eq!(
1582            PathElementCS::from_bytes(input).unwrap_err().kind(),
1583            ErrorKind::InvalidUtf8
1584        );
1585    }
1586
1587    #[test]
1588    fn from_bytes_lone_low_surrogate_rejected() {
1589        let input: &[u8] = &[0x61, 0xED, 0xB0, 0x80, 0x62];
1590        assert_eq!(
1591            PathElementCS::from_bytes(input).unwrap_err().kind(),
1592            ErrorKind::InvalidUtf8
1593        );
1594    }
1595
1596    #[test]
1597    fn from_bytes_overlong_null_only() {
1598        let input: &[u8] = &[0xC0, 0x80];
1599        assert_eq!(
1600            PathElementCS::from_bytes(input).unwrap_err().kind(),
1601            ErrorKind::InvalidUtf8
1602        );
1603    }
1604
1605    #[test]
1606    fn from_bytes_invalid_byte_rejected() {
1607        let input: &[u8] = &[0x68, 0x69, 0xFF];
1608        assert_eq!(
1609            PathElementCS::from_bytes(input).unwrap_err().kind(),
1610            ErrorKind::InvalidUtf8
1611        );
1612    }
1613
1614    // --- os_compatible supplementary character tests ---
1615
1616    #[test]
1617    fn os_compatible_supplementary_unchanged() {
1618        let pe = PathElementCS::new("file_😀.txt").unwrap();
1619        assert_eq!(pe.os_compatible(), "file_😀.txt");
1620    }
1621
1622    #[test]
1623    fn os_compatible_supplementary_roundtrip() {
1624        let pe = PathElementCS::new("file_😀.txt").unwrap();
1625        let pe2 = PathElementCS::new(pe.os_compatible()).unwrap();
1626        assert_eq!(pe.normalized(), pe2.normalized());
1627    }
1628
1629    #[test]
1630    fn os_compatible_multiple_supplementary() {
1631        let pe = PathElementCS::new("𐀀_𝄞_😀").unwrap();
1632        assert_eq!(pe.os_compatible(), "𐀀_𝄞_😀");
1633    }
1634}
1635
1636#[cfg(all(test, feature = "std"))]
1637mod os_str_tests {
1638    use std::borrow::Cow;
1639    use std::ffi::{OsStr, OsString};
1640
1641    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
1642    use wasm_bindgen_test::wasm_bindgen_test as test;
1643
1644    use crate::ErrorKind;
1645    use crate::case_sensitivity::{CaseInsensitive, CaseSensitivity};
1646    use crate::path_element::{PathElement, PathElementCI, PathElementCS};
1647
1648    #[test]
1649    fn from_os_str_borrowed_matches_new() {
1650        let input = OsStr::new("hello.txt");
1651        let from_os = PathElementCS::from_os_str(input).unwrap();
1652        let from_new = PathElementCS::new("hello.txt").unwrap();
1653        assert_eq!(from_os.original(), from_new.original());
1654        assert_eq!(from_os.normalized(), from_new.normalized());
1655        assert_eq!(from_os.os_compatible(), from_new.os_compatible());
1656    }
1657
1658    #[test]
1659    fn from_os_str_owned_matches_new() {
1660        let input = OsString::from("Hello.txt");
1661        let from_os = PathElementCI::from_os_str(input).unwrap();
1662        let from_new = PathElementCI::new("Hello.txt").unwrap();
1663        assert_eq!(from_os.original(), from_new.original());
1664        assert_eq!(from_os.normalized(), from_new.normalized());
1665        assert_eq!(from_os.os_compatible(), from_new.os_compatible());
1666    }
1667
1668    #[test]
1669    fn from_os_str_borrowed_preserves_borrow() {
1670        let input = OsStr::new("hello.txt");
1671        let pe = PathElementCS::from_os_str(input).unwrap();
1672        let orig = pe.into_original();
1673        assert!(matches!(orig, Cow::Borrowed(_)));
1674    }
1675
1676    #[test]
1677    fn from_os_str_rejects_empty() {
1678        assert_eq!(
1679            PathElementCS::from_os_str(OsStr::new(""))
1680                .unwrap_err()
1681                .kind(),
1682            ErrorKind::Empty
1683        );
1684    }
1685
1686    #[test]
1687    fn from_os_str_rejects_dot() {
1688        assert_eq!(
1689            PathElementCS::from_os_str(OsStr::new("."))
1690                .unwrap_err()
1691                .kind(),
1692            ErrorKind::CurrentDirectoryMarker
1693        );
1694    }
1695
1696    #[test]
1697    fn from_os_str_rejects_dotdot() {
1698        assert_eq!(
1699            PathElementCS::from_os_str(OsStr::new(".."))
1700                .unwrap_err()
1701                .kind(),
1702            ErrorKind::ParentDirectoryMarker
1703        );
1704    }
1705
1706    #[test]
1707    fn from_os_str_rejects_slash() {
1708        assert_eq!(
1709            PathElementCS::from_os_str(OsStr::new("a/b"))
1710                .unwrap_err()
1711                .kind(),
1712            ErrorKind::ContainsForwardSlash
1713        );
1714    }
1715
1716    #[test]
1717    fn from_os_str_rejects_null() {
1718        assert_eq!(
1719            PathElementCS::from_os_str(OsStr::new("\0"))
1720                .unwrap_err()
1721                .kind(),
1722            ErrorKind::ContainsNullByte
1723        );
1724    }
1725
1726    #[test]
1727    fn from_os_str_rejects_unassigned() {
1728        assert_eq!(
1729            PathElementCS::from_os_str(OsStr::new("\u{0378}"))
1730                .unwrap_err()
1731                .kind(),
1732            ErrorKind::ContainsUnassignedChar
1733        );
1734    }
1735
1736    #[cfg(unix)]
1737    #[test]
1738    fn from_os_str_invalid_utf8_borrowed_rejected() {
1739        use std::os::unix::ffi::OsStrExt;
1740        let input = OsStr::from_bytes(&[0x68, 0x69, 0xFF]);
1741        assert_eq!(
1742            PathElementCS::from_os_str(input).unwrap_err().kind(),
1743            ErrorKind::InvalidUtf8
1744        );
1745    }
1746
1747    #[cfg(unix)]
1748    #[test]
1749    fn from_os_str_invalid_utf8_owned_rejected() {
1750        use std::os::unix::ffi::OsStrExt;
1751        let input = OsStr::from_bytes(&[0x68, 0x69, 0xFF]).to_os_string();
1752        assert_eq!(
1753            PathElementCS::from_os_str(input).unwrap_err().kind(),
1754            ErrorKind::InvalidUtf8
1755        );
1756    }
1757
1758    #[cfg(unix)]
1759    #[test]
1760    fn from_os_str_surrogate_bytes_rejected() {
1761        use std::os::unix::ffi::OsStrExt;
1762        // ED A0 80 is the WTF-8 encoding of unpaired surrogate U+D800.
1763        let input = OsStr::from_bytes(&[0x68, 0xED, 0xA0, 0x80, 0x69]);
1764        assert_eq!(
1765            PathElementCS::from_os_str(input).unwrap_err().kind(),
1766            ErrorKind::InvalidUtf8
1767        );
1768    }
1769
1770    #[cfg(windows)]
1771    #[test]
1772    fn from_os_str_invalid_utf8_borrowed_rejected() {
1773        use std::os::windows::ffi::OsStringExt;
1774        let input = OsString::from_wide(&[0x68, 0xD800, 0x69]);
1775        assert_eq!(
1776            PathElementCS::from_os_str(input.as_os_str())
1777                .unwrap_err()
1778                .kind(),
1779            ErrorKind::InvalidUtf8
1780        );
1781    }
1782
1783    #[cfg(windows)]
1784    #[test]
1785    fn from_os_str_invalid_utf8_owned_rejected() {
1786        use std::os::windows::ffi::OsStringExt;
1787        let input = OsString::from_wide(&[0x68, 0xD800, 0x69]);
1788        assert_eq!(
1789            PathElementCS::from_os_str(input).unwrap_err().kind(),
1790            ErrorKind::InvalidUtf8
1791        );
1792    }
1793
1794    // CI "H\tllo": original="H\tllo", os_compatible="H␉llo", normalized="h␉llo"
1795    // All three differ, so asserting "H\u{2409}llo" proves it's os_compatible.
1796    #[test]
1797    fn os_str_returns_os_compatible() {
1798        let pe = PathElementCI::new("H\tllo").unwrap();
1799        assert_eq!(pe.os_str(), OsStr::new("H\u{2409}llo"));
1800    }
1801
1802    #[test]
1803    fn into_os_str_returns_os_compatible() {
1804        let pe = PathElementCI::new("H\tllo").unwrap();
1805        let result = pe.into_os_str();
1806        assert_eq!(result, OsStr::new("H\u{2409}llo"));
1807    }
1808
1809    #[test]
1810    fn into_os_str_borrows_when_no_transformation() {
1811        let input = OsStr::new("hello.txt");
1812        let pe = PathElementCS::from_os_str(input).unwrap();
1813        let result = pe.into_os_str();
1814        assert!(matches!(result, Cow::Borrowed(_)));
1815        assert_eq!(result, OsStr::new("hello.txt"));
1816    }
1817
1818    #[test]
1819    fn into_os_str_ci_borrows_when_already_folded() {
1820        let input = OsStr::new("hello.txt");
1821        let pe = PathElementCI::from_os_str(input).unwrap();
1822        let result = pe.into_os_str();
1823        assert!(matches!(result, Cow::Borrowed(_)));
1824        assert_eq!(result, OsStr::new("hello.txt"));
1825    }
1826
1827    // Borrowed input, but NFC normalization produces owned output.
1828    // On Apple, the os-compatible form is NFD, which matches the NFD input, so it borrows.
1829    #[test]
1830    fn into_os_str_owned_when_nfc_transforms() {
1831        let input = OsStr::new("e\u{0301}.txt"); // NFD
1832        let pe = PathElementCS::from_os_str(input).unwrap();
1833        let result = pe.into_os_str();
1834        #[cfg(target_vendor = "apple")]
1835        assert!(matches!(result, Cow::Borrowed(_)));
1836        #[cfg(not(target_vendor = "apple"))]
1837        assert!(matches!(result, Cow::Owned(_)));
1838    }
1839
1840    // NFC input borrows on non-Apple (os-compatible is NFC), owned on Apple (os-compatible is NFD).
1841    #[test]
1842    fn into_os_str_owned_when_nfd_transforms() {
1843        let input = OsStr::new("\u{00E9}.txt"); // NFC
1844        let pe = PathElementCS::from_os_str(input).unwrap();
1845        let result = pe.into_os_str();
1846        #[cfg(target_vendor = "apple")]
1847        assert!(matches!(result, Cow::Owned(_)));
1848        #[cfg(not(target_vendor = "apple"))]
1849        assert!(matches!(result, Cow::Borrowed(_)));
1850    }
1851
1852    #[test]
1853    fn from_os_str_cs_matches_typed() {
1854        let input = OsStr::new("Hello.txt");
1855        let dyn_pe = PathElement::from_os_str_cs(input).unwrap();
1856        let cs_pe = PathElementCS::from_os_str(input).unwrap();
1857        assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
1858        assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
1859        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
1860    }
1861
1862    #[test]
1863    fn from_os_str_ci_matches_typed() {
1864        let input = OsStr::new("Hello.txt");
1865        let dyn_pe = PathElement::from_os_str_ci(input).unwrap();
1866        let ci_pe = PathElementCI::from_os_str(input).unwrap();
1867        assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
1868        assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
1869        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
1870    }
1871
1872    #[test]
1873    fn from_os_str_dynamic_case_sensitivity() {
1874        let input = OsStr::new("Hello.txt");
1875        let pe = PathElement::from_os_str(input, CaseInsensitive).unwrap();
1876        assert_eq!(pe.normalized(), "hello.txt");
1877    }
1878}