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!(
1043            *PathElementCS::new("").unwrap_err().kind(),
1044            ErrorKind::Empty
1045        );
1046    }
1047
1048    #[test]
1049    fn new_rejects_dot() {
1050        assert_eq!(
1051            *PathElementCS::new(".").unwrap_err().kind(),
1052            ErrorKind::CurrentDirectoryMarker
1053        );
1054    }
1055
1056    #[test]
1057    fn new_rejects_dotdot() {
1058        assert_eq!(
1059            *PathElementCS::new("..").unwrap_err().kind(),
1060            ErrorKind::ParentDirectoryMarker
1061        );
1062    }
1063
1064    #[test]
1065    fn new_rejects_slash() {
1066        assert_eq!(
1067            *PathElementCS::new("a/b").unwrap_err().kind(),
1068            ErrorKind::ContainsForwardSlash
1069        );
1070    }
1071
1072    #[test]
1073    fn new_rejects_null() {
1074        assert_eq!(
1075            *PathElementCS::new("\0").unwrap_err().kind(),
1076            ErrorKind::ContainsNullByte
1077        );
1078    }
1079
1080    #[test]
1081    fn new_rejects_unassigned() {
1082        assert_eq!(
1083            *PathElementCS::new("\u{0378}").unwrap_err().kind(),
1084            ErrorKind::ContainsUnassignedChar
1085        );
1086    }
1087
1088    // --- PartialEq / Eq ---
1089
1090    #[test]
1091    fn path_element_eq_same_cs() {
1092        let a = PathElementCS::new("hello.txt").unwrap();
1093        let b = PathElementCS::new("hello.txt").unwrap();
1094        assert_eq!(a, b);
1095    }
1096
1097    #[test]
1098    fn path_element_eq_different_original_same_normalized_cs() {
1099        let a = PathElementCS::new("  hello.txt  ").unwrap();
1100        let b = PathElementCS::new("hello.txt").unwrap();
1101        assert_ne!(a.original(), b.original());
1102        assert_eq!(a, b);
1103    }
1104
1105    #[test]
1106    fn path_element_ne_different_case_cs() {
1107        let a = PathElementCS::new("Hello.txt").unwrap();
1108        let b = PathElementCS::new("hello.txt").unwrap();
1109        assert_ne!(a, b);
1110    }
1111
1112    #[test]
1113    fn path_element_eq_different_case_ci() {
1114        let a = PathElementCI::new("Hello.txt").unwrap();
1115        let b = PathElementCI::new("hello.txt").unwrap();
1116        assert_eq!(a, b);
1117    }
1118
1119    #[test]
1120    fn path_element_eq_nfc_nfd_cs() {
1121        let a = PathElementCS::new("\u{00E9}.txt").unwrap();
1122        let b = PathElementCS::new("e\u{0301}.txt").unwrap();
1123        assert_eq!(a, b);
1124    }
1125
1126    #[test]
1127    fn path_element_eq_cross_lifetime() {
1128        let owned = PathElementCS::new("hello.txt").unwrap().into_owned();
1129        let input = "hello.txt";
1130        let borrowed = PathElementCS::new(Cow::Borrowed(input)).unwrap();
1131        assert_eq!(owned, borrowed);
1132        assert_eq!(borrowed, owned);
1133    }
1134
1135    #[test]
1136    #[should_panic(expected = "different case sensitivity")]
1137    fn path_element_eq_panics_on_mixed_dynamic_case_sensitivity() {
1138        let a = PathElement::new("hello", CaseSensitive).unwrap();
1139        let b = PathElement::new("hello", CaseInsensitive).unwrap();
1140        let _ = a == b;
1141    }
1142
1143    // --- PartialOrd / Ord ---
1144
1145    #[test]
1146    fn path_element_ord_alphabetical_cs() {
1147        let a = PathElementCS::new("apple").unwrap();
1148        let b = PathElementCS::new("banana").unwrap();
1149        assert!(a < b);
1150        assert!(b > a);
1151    }
1152
1153    #[test]
1154    fn path_element_ord_equal_cs() {
1155        let a = PathElementCS::new("hello").unwrap();
1156        let b = PathElementCS::new("hello").unwrap();
1157        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
1158    }
1159
1160    #[test]
1161    fn path_element_ord_case_ci() {
1162        let a = PathElementCI::new("Apple").unwrap();
1163        let b = PathElementCI::new("apple").unwrap();
1164        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
1165    }
1166
1167    #[test]
1168    fn path_element_partial_ord_cross_lifetime() {
1169        let owned = PathElementCS::new("apple").unwrap().into_owned();
1170        let input = "banana";
1171        let borrowed = PathElementCS::new(Cow::Borrowed(input)).unwrap();
1172        assert!(owned < borrowed);
1173    }
1174
1175    #[test]
1176    fn path_element_partial_ord_none_on_mixed_dynamic_case_sensitivity() {
1177        let a = PathElement::new("hello", CaseSensitive).unwrap();
1178        let b = PathElement::new("hello", CaseInsensitive).unwrap();
1179        assert_eq!(a.partial_cmp(&b), None);
1180    }
1181
1182    #[test]
1183    fn path_element_ord_sortable() {
1184        let mut elems: Vec<_> = ["cherry", "apple", "banana"]
1185            .iter()
1186            .map(|s| PathElementCS::new(Cow::Borrowed(*s)).unwrap())
1187            .collect();
1188        elems.sort();
1189        let names: Vec<_> = elems.iter().map(PathElementCS::normalized).collect();
1190        assert_eq!(names, &["apple", "banana", "cherry"]);
1191    }
1192
1193    #[test]
1194    fn path_element_ord_ci_sortable() {
1195        let mut elems: Vec<_> = ["Cherry", "apple", "BANANA"]
1196            .iter()
1197            .map(|s| PathElementCI::new(Cow::Borrowed(*s)).unwrap())
1198            .collect();
1199        elems.sort();
1200        let names: Vec<_> = elems.iter().map(PathElementCI::normalized).collect();
1201        assert_eq!(names, &["apple", "banana", "cherry"]);
1202    }
1203
1204    // --- Conversions ---
1205
1206    #[test]
1207    fn from_cs_into_dynamic() {
1208        let pe = PathElementCS::new("hello").unwrap();
1209        let dyn_pe: PathElement<'_> = pe.into();
1210        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
1211        assert_eq!(dyn_pe.normalized(), "hello");
1212    }
1213
1214    #[test]
1215    fn from_ci_into_dynamic() {
1216        let pe = PathElementCI::new("Hello").unwrap();
1217        let dyn_pe: PathElement<'_> = pe.into();
1218        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
1219        assert_eq!(dyn_pe.normalized(), "hello");
1220    }
1221
1222    #[test]
1223    fn try_from_dynamic_to_cs() {
1224        let pe = PathElement::new("hello", CaseSensitive).unwrap();
1225        let cs_pe: PathElementCS<'_> = pe.try_into().unwrap();
1226        assert_eq!(cs_pe.normalized(), "hello");
1227    }
1228
1229    #[test]
1230    fn try_from_dynamic_to_cs_wrong_variant() {
1231        let pe = PathElement::new("Hello", CaseInsensitive).unwrap();
1232        let err: PathElementCI<'_> = PathElementCS::try_from(pe).unwrap_err();
1233        assert_eq!(err.original(), "Hello");
1234        assert_eq!(err.normalized(), "hello");
1235        assert_eq!(err.os_compatible(), "Hello");
1236    }
1237
1238    #[test]
1239    fn try_from_dynamic_to_ci() {
1240        let pe = PathElement::new("Hello", CaseInsensitive).unwrap();
1241        let ci_pe: PathElementCI<'_> = pe.try_into().unwrap();
1242        assert_eq!(ci_pe.normalized(), "hello");
1243    }
1244
1245    // --- PathElement convenience constructors ---
1246
1247    #[test]
1248    fn dyn_new_cs() {
1249        let pe = PathElement::new_cs("Hello.txt").unwrap();
1250        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
1251        assert_eq!(pe.normalized(), "Hello.txt");
1252    }
1253
1254    #[test]
1255    fn dyn_new_ci() {
1256        let pe = PathElement::new_ci("Hello.txt").unwrap();
1257        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
1258        assert_eq!(pe.normalized(), "hello.txt");
1259    }
1260
1261    #[test]
1262    fn dyn_new_cs_matches_typed() {
1263        let dyn_pe = PathElement::new_cs("Hello.txt").unwrap();
1264        let cs_pe = PathElementCS::new("Hello.txt").unwrap();
1265        assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
1266        assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
1267    }
1268
1269    #[test]
1270    fn dyn_new_ci_matches_typed() {
1271        let dyn_pe = PathElement::new_ci("Hello.txt").unwrap();
1272        let ci_pe = PathElementCI::new("Hello.txt").unwrap();
1273        assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
1274        assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
1275    }
1276
1277    // --- case_sensitivity() getter ---
1278
1279    #[test]
1280    fn case_sensitivity_cs() {
1281        let pe = PathElementCS::new("hello").unwrap();
1282        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
1283    }
1284
1285    #[test]
1286    fn case_sensitivity_ci() {
1287        let pe = PathElementCI::new("hello").unwrap();
1288        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
1289    }
1290
1291    #[test]
1292    fn case_sensitivity_dyn() {
1293        let cs = PathElement::new("hello", CaseSensitive).unwrap();
1294        let ci = PathElement::new("hello", CaseInsensitive).unwrap();
1295        assert_eq!(cs.case_sensitivity(), CaseSensitivity::Sensitive);
1296        assert_eq!(ci.case_sensitivity(), CaseSensitivity::Insensitive);
1297    }
1298
1299    // --- PartialOrd returns None on mismatch ---
1300
1301    #[test]
1302    fn partial_ord_dyn_same_case_sensitivity() {
1303        let a = PathElement::new("apple", CaseSensitive).unwrap();
1304        let b = PathElement::new("banana", CaseSensitive).unwrap();
1305        assert!(a < b);
1306    }
1307
1308    #[test]
1309    fn partial_ord_dyn_none_on_mismatch() {
1310        let a = PathElement::new("hello", CaseSensitive).unwrap();
1311        let b = PathElement::new("hello", CaseInsensitive).unwrap();
1312        assert_eq!(a.partial_cmp(&b), None);
1313    }
1314
1315    // --- TryFrom error returns original ---
1316
1317    #[test]
1318    fn try_from_dynamic_to_ci_wrong_variant() {
1319        let pe = PathElement::new("Hello", CaseSensitive).unwrap();
1320        let err: PathElementCS<'_> = PathElementCI::try_from(pe).unwrap_err();
1321        assert_eq!(err.original(), "Hello");
1322        assert_eq!(err.normalized(), "Hello");
1323        assert_eq!(err.os_compatible(), "Hello");
1324    }
1325
1326    // --- into_owned preserves case_sensitivity ---
1327
1328    #[test]
1329    fn into_owned_preserves_cs_case_sensitivity() {
1330        let pe = PathElementCS::new("hello").unwrap();
1331        let owned = pe.into_owned();
1332        assert_eq!(owned.case_sensitivity(), CaseSensitivity::Sensitive);
1333    }
1334
1335    #[test]
1336    fn into_owned_preserves_dyn_case_sensitivity() {
1337        let pe = PathElement::new("hello", CaseInsensitive).unwrap();
1338        let owned = pe.into_owned();
1339        assert_eq!(owned.case_sensitivity(), CaseSensitivity::Insensitive);
1340    }
1341
1342    // --- Cross-type PartialEq ---
1343
1344    #[test]
1345    fn eq_cs_vs_dyn_same_case_sensitivity() {
1346        let cs = PathElementCS::new("hello").unwrap();
1347        let dyn_cs = PathElement::new_cs("hello").unwrap();
1348        assert_eq!(cs, dyn_cs);
1349        assert_eq!(dyn_cs, cs);
1350    }
1351
1352    #[test]
1353    fn eq_ci_vs_dyn_same_case_sensitivity() {
1354        let ci = PathElementCI::new("Hello").unwrap();
1355        let dyn_ci = PathElement::new_ci("hello").unwrap();
1356        assert_eq!(ci, dyn_ci);
1357        assert_eq!(dyn_ci, ci);
1358    }
1359
1360    #[test]
1361    #[should_panic(expected = "different case sensitivity")]
1362    fn eq_cs_vs_ci_panics() {
1363        let cs = PathElementCS::new("hello").unwrap();
1364        let ci = PathElementCI::new("hello").unwrap();
1365        let _ = cs == ci;
1366    }
1367
1368    #[test]
1369    #[should_panic(expected = "different case sensitivity")]
1370    fn eq_cs_vs_dyn_ci_panics() {
1371        let cs = PathElementCS::new("hello").unwrap();
1372        let dyn_ci = PathElement::new_ci("hello").unwrap();
1373        let _ = cs == dyn_ci;
1374    }
1375
1376    // --- Cross-type PartialOrd ---
1377
1378    #[test]
1379    fn partial_ord_cs_vs_dyn_same_case_sensitivity() {
1380        let cs = PathElementCS::new("apple").unwrap();
1381        let dyn_cs = PathElement::new_cs("banana").unwrap();
1382        assert!(cs < dyn_cs);
1383        assert!(dyn_cs > cs);
1384    }
1385
1386    #[test]
1387    fn partial_ord_cs_vs_ci_none() {
1388        let cs = PathElementCS::new("hello").unwrap();
1389        let ci = PathElementCI::new("hello").unwrap();
1390        assert_eq!(cs.partial_cmp(&ci), None);
1391        assert_eq!(ci.partial_cmp(&cs), None);
1392    }
1393
1394    #[test]
1395    fn partial_ord_cs_vs_dyn_ci_none() {
1396        let cs = PathElementCS::new("hello").unwrap();
1397        let dyn_ci = PathElement::new_ci("hello").unwrap();
1398        assert_eq!(cs.partial_cmp(&dyn_ci), None);
1399        assert_eq!(dyn_ci.partial_cmp(&cs), None);
1400    }
1401
1402    // --- from_bytes ---
1403
1404    #[test]
1405    fn from_bytes_cs_borrowed_matches_new() {
1406        let pe_bytes = PathElementCS::from_bytes(b"hello.txt" as &[u8]).unwrap();
1407        let pe_str = PathElementCS::new("hello.txt").unwrap();
1408        assert_eq!(pe_bytes.original(), pe_str.original());
1409        assert_eq!(pe_bytes.normalized(), pe_str.normalized());
1410        assert_eq!(pe_bytes.os_compatible(), pe_str.os_compatible());
1411    }
1412
1413    #[test]
1414    fn from_bytes_ci_borrowed_matches_new() {
1415        let pe_bytes = PathElementCI::from_bytes(b"Hello.txt" as &[u8]).unwrap();
1416        let pe_str = PathElementCI::new("Hello.txt").unwrap();
1417        assert_eq!(pe_bytes.original(), pe_str.original());
1418        assert_eq!(pe_bytes.normalized(), pe_str.normalized());
1419        assert_eq!(pe_bytes.os_compatible(), pe_str.os_compatible());
1420    }
1421
1422    #[test]
1423    fn from_bytes_owned_matches_new() {
1424        let pe_bytes = PathElementCS::from_bytes(b"hello.txt".to_vec()).unwrap();
1425        let pe_str = PathElementCS::new("hello.txt").unwrap();
1426        assert_eq!(pe_bytes.original(), pe_str.original());
1427        assert_eq!(pe_bytes.normalized(), pe_str.normalized());
1428    }
1429
1430    #[test]
1431    fn from_bytes_borrowed_preserves_borrow() {
1432        let input: &[u8] = b"hello.txt";
1433        let pe = PathElementCS::from_bytes(input).unwrap();
1434        let orig = pe.into_original();
1435        assert!(matches!(orig, Cow::Borrowed(_)));
1436    }
1437
1438    #[test]
1439    fn from_bytes_owned_is_owned() {
1440        let pe = PathElementCS::from_bytes(b"hello.txt".to_vec()).unwrap();
1441        assert!(pe.is_owned());
1442    }
1443
1444    #[test]
1445    fn from_bytes_invalid_utf8_borrowed_rejected() {
1446        let input: &[u8] = &[0x68, 0x69, 0xFF]; // "hi" + invalid byte
1447        let err = PathElementCS::from_bytes(input).unwrap_err();
1448        assert_eq!(*err.kind(), ErrorKind::InvalidUtf8);
1449    }
1450
1451    #[test]
1452    fn from_bytes_invalid_utf8_owned_rejected() {
1453        let input = vec![0x68, 0x69, 0xFF];
1454        let err = PathElementCS::from_bytes(input).unwrap_err();
1455        assert_eq!(*err.kind(), ErrorKind::InvalidUtf8);
1456    }
1457
1458    #[test]
1459    fn from_bytes_dynamic_case_sensitivity() {
1460        let pe = PathElement::from_bytes(b"Hello.txt" as &[u8], CaseInsensitive).unwrap();
1461        assert_eq!(pe.normalized(), "hello.txt");
1462        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
1463    }
1464
1465    #[test]
1466    fn from_bytes_cs_matches_typed() {
1467        let input: &[u8] = b"Hello.txt";
1468        let dyn_pe = PathElement::from_bytes_cs(input).unwrap();
1469        let cs_pe = PathElementCS::from_bytes(input).unwrap();
1470        assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
1471        assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
1472        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
1473    }
1474
1475    #[test]
1476    fn from_bytes_ci_matches_typed() {
1477        let input: &[u8] = b"Hello.txt";
1478        let dyn_pe = PathElement::from_bytes_ci(input).unwrap();
1479        let ci_pe = PathElementCI::from_bytes(input).unwrap();
1480        assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
1481        assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
1482        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
1483    }
1484
1485    #[test]
1486    fn from_bytes_with_case_sensitivity_cs() {
1487        let pe = PathElementCS::from_bytes(b"Hello.txt" as &[u8]).unwrap();
1488        assert_eq!(pe.normalized(), "Hello.txt");
1489    }
1490
1491    #[test]
1492    fn from_bytes_with_case_sensitivity_ci() {
1493        let pe = PathElementCI::from_bytes(b"Hello.txt" as &[u8]).unwrap();
1494        assert_eq!(pe.normalized(), "hello.txt");
1495    }
1496
1497    #[test]
1498    fn from_bytes_rejects_empty() {
1499        assert_eq!(
1500            *PathElementCS::from_bytes(b"" as &[u8]).unwrap_err().kind(),
1501            ErrorKind::Empty
1502        );
1503    }
1504
1505    #[test]
1506    fn from_bytes_rejects_dot() {
1507        assert_eq!(
1508            *PathElementCS::from_bytes(b"." as &[u8]).unwrap_err().kind(),
1509            ErrorKind::CurrentDirectoryMarker
1510        );
1511    }
1512
1513    #[test]
1514    fn from_bytes_rejects_dotdot() {
1515        assert_eq!(
1516            *PathElementCS::from_bytes(b".." as &[u8])
1517                .unwrap_err()
1518                .kind(),
1519            ErrorKind::ParentDirectoryMarker
1520        );
1521    }
1522
1523    #[test]
1524    fn from_bytes_rejects_slash() {
1525        assert_eq!(
1526            *PathElementCS::from_bytes(b"a/b" as &[u8])
1527                .unwrap_err()
1528                .kind(),
1529            ErrorKind::ContainsForwardSlash
1530        );
1531    }
1532
1533    #[test]
1534    fn from_bytes_rejects_null() {
1535        assert_eq!(
1536            *PathElementCS::from_bytes(b"\0" as &[u8])
1537                .unwrap_err()
1538                .kind(),
1539            ErrorKind::ContainsNullByte
1540        );
1541    }
1542
1543    #[test]
1544    fn from_bytes_rejects_unassigned() {
1545        assert_eq!(
1546            *PathElementCS::from_bytes("\u{0378}".as_bytes())
1547                .unwrap_err()
1548                .kind(),
1549            ErrorKind::ContainsUnassignedChar
1550        );
1551    }
1552
1553    #[test]
1554    fn from_bytes_dynamic_sensitive() {
1555        let pe = PathElement::from_bytes(b"Hello.txt" as &[u8], CaseSensitive).unwrap();
1556        assert_eq!(pe.normalized(), "Hello.txt");
1557        assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
1558    }
1559
1560    // --- Invalid UTF-8 rejection ---
1561
1562    #[test]
1563    fn from_bytes_overlong_null() {
1564        // 0xC0 0x80 is an overlong encoding — rejected.
1565        let input: &[u8] = &[0x61, 0xC0, 0x80, 0x62];
1566        assert_eq!(
1567            *PathElementCS::from_bytes(input).unwrap_err().kind(),
1568            ErrorKind::InvalidUtf8
1569        );
1570    }
1571
1572    #[test]
1573    fn from_bytes_surrogate_bytes_rejected() {
1574        let input: &[u8] = &[0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80];
1575        assert_eq!(
1576            *PathElementCS::from_bytes(input).unwrap_err().kind(),
1577            ErrorKind::InvalidUtf8
1578        );
1579    }
1580
1581    #[test]
1582    fn from_bytes_lone_high_surrogate_rejected() {
1583        let input: &[u8] = &[0x61, 0xED, 0xA0, 0x80, 0x62];
1584        assert_eq!(
1585            *PathElementCS::from_bytes(input).unwrap_err().kind(),
1586            ErrorKind::InvalidUtf8
1587        );
1588    }
1589
1590    #[test]
1591    fn from_bytes_lone_low_surrogate_rejected() {
1592        let input: &[u8] = &[0x61, 0xED, 0xB0, 0x80, 0x62];
1593        assert_eq!(
1594            *PathElementCS::from_bytes(input).unwrap_err().kind(),
1595            ErrorKind::InvalidUtf8
1596        );
1597    }
1598
1599    #[test]
1600    fn from_bytes_overlong_null_only() {
1601        let input: &[u8] = &[0xC0, 0x80];
1602        assert_eq!(
1603            *PathElementCS::from_bytes(input).unwrap_err().kind(),
1604            ErrorKind::InvalidUtf8
1605        );
1606    }
1607
1608    #[test]
1609    fn from_bytes_invalid_byte_rejected() {
1610        let input: &[u8] = &[0x68, 0x69, 0xFF];
1611        assert_eq!(
1612            *PathElementCS::from_bytes(input).unwrap_err().kind(),
1613            ErrorKind::InvalidUtf8
1614        );
1615    }
1616
1617    // --- os_compatible supplementary character tests ---
1618
1619    #[test]
1620    fn os_compatible_supplementary_unchanged() {
1621        let pe = PathElementCS::new("file_😀.txt").unwrap();
1622        assert_eq!(pe.os_compatible(), "file_😀.txt");
1623    }
1624
1625    #[test]
1626    fn os_compatible_supplementary_roundtrip() {
1627        let pe = PathElementCS::new("file_😀.txt").unwrap();
1628        let pe2 = PathElementCS::new(pe.os_compatible()).unwrap();
1629        assert_eq!(pe.normalized(), pe2.normalized());
1630    }
1631
1632    #[test]
1633    fn os_compatible_multiple_supplementary() {
1634        let pe = PathElementCS::new("𐀀_𝄞_😀").unwrap();
1635        assert_eq!(pe.os_compatible(), "𐀀_𝄞_😀");
1636    }
1637}
1638
1639#[cfg(all(test, feature = "std"))]
1640mod os_str_tests {
1641    use std::borrow::Cow;
1642    use std::ffi::{OsStr, OsString};
1643
1644    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
1645    use wasm_bindgen_test::wasm_bindgen_test as test;
1646
1647    use crate::ErrorKind;
1648    use crate::case_sensitivity::{CaseInsensitive, CaseSensitivity};
1649    use crate::path_element::{PathElement, PathElementCI, PathElementCS};
1650
1651    #[test]
1652    fn from_os_str_borrowed_matches_new() {
1653        let input = OsStr::new("hello.txt");
1654        let from_os = PathElementCS::from_os_str(input).unwrap();
1655        let from_new = PathElementCS::new("hello.txt").unwrap();
1656        assert_eq!(from_os.original(), from_new.original());
1657        assert_eq!(from_os.normalized(), from_new.normalized());
1658        assert_eq!(from_os.os_compatible(), from_new.os_compatible());
1659    }
1660
1661    #[test]
1662    fn from_os_str_owned_matches_new() {
1663        let input = OsString::from("Hello.txt");
1664        let from_os = PathElementCI::from_os_str(input).unwrap();
1665        let from_new = PathElementCI::new("Hello.txt").unwrap();
1666        assert_eq!(from_os.original(), from_new.original());
1667        assert_eq!(from_os.normalized(), from_new.normalized());
1668        assert_eq!(from_os.os_compatible(), from_new.os_compatible());
1669    }
1670
1671    #[test]
1672    fn from_os_str_borrowed_preserves_borrow() {
1673        let input = OsStr::new("hello.txt");
1674        let pe = PathElementCS::from_os_str(input).unwrap();
1675        let orig = pe.into_original();
1676        assert!(matches!(orig, Cow::Borrowed(_)));
1677    }
1678
1679    #[test]
1680    fn from_os_str_rejects_empty() {
1681        assert_eq!(
1682            *PathElementCS::from_os_str(OsStr::new(""))
1683                .unwrap_err()
1684                .kind(),
1685            ErrorKind::Empty
1686        );
1687    }
1688
1689    #[test]
1690    fn from_os_str_rejects_dot() {
1691        assert_eq!(
1692            *PathElementCS::from_os_str(OsStr::new("."))
1693                .unwrap_err()
1694                .kind(),
1695            ErrorKind::CurrentDirectoryMarker
1696        );
1697    }
1698
1699    #[test]
1700    fn from_os_str_rejects_dotdot() {
1701        assert_eq!(
1702            *PathElementCS::from_os_str(OsStr::new(".."))
1703                .unwrap_err()
1704                .kind(),
1705            ErrorKind::ParentDirectoryMarker
1706        );
1707    }
1708
1709    #[test]
1710    fn from_os_str_rejects_slash() {
1711        assert_eq!(
1712            *PathElementCS::from_os_str(OsStr::new("a/b"))
1713                .unwrap_err()
1714                .kind(),
1715            ErrorKind::ContainsForwardSlash
1716        );
1717    }
1718
1719    #[test]
1720    fn from_os_str_rejects_null() {
1721        assert_eq!(
1722            *PathElementCS::from_os_str(OsStr::new("\0"))
1723                .unwrap_err()
1724                .kind(),
1725            ErrorKind::ContainsNullByte
1726        );
1727    }
1728
1729    #[test]
1730    fn from_os_str_rejects_unassigned() {
1731        assert_eq!(
1732            *PathElementCS::from_os_str(OsStr::new("\u{0378}"))
1733                .unwrap_err()
1734                .kind(),
1735            ErrorKind::ContainsUnassignedChar
1736        );
1737    }
1738
1739    #[cfg(unix)]
1740    #[test]
1741    fn from_os_str_invalid_utf8_borrowed_rejected() {
1742        use std::os::unix::ffi::OsStrExt;
1743        let input = OsStr::from_bytes(&[0x68, 0x69, 0xFF]);
1744        assert_eq!(
1745            *PathElementCS::from_os_str(input).unwrap_err().kind(),
1746            ErrorKind::InvalidUtf8
1747        );
1748    }
1749
1750    #[cfg(unix)]
1751    #[test]
1752    fn from_os_str_invalid_utf8_owned_rejected() {
1753        use std::os::unix::ffi::OsStrExt;
1754        let input = OsStr::from_bytes(&[0x68, 0x69, 0xFF]).to_os_string();
1755        assert_eq!(
1756            *PathElementCS::from_os_str(input).unwrap_err().kind(),
1757            ErrorKind::InvalidUtf8
1758        );
1759    }
1760
1761    #[cfg(unix)]
1762    #[test]
1763    fn from_os_str_surrogate_bytes_rejected() {
1764        use std::os::unix::ffi::OsStrExt;
1765        // ED A0 80 is the WTF-8 encoding of unpaired surrogate U+D800.
1766        let input = OsStr::from_bytes(&[0x68, 0xED, 0xA0, 0x80, 0x69]);
1767        assert_eq!(
1768            *PathElementCS::from_os_str(input).unwrap_err().kind(),
1769            ErrorKind::InvalidUtf8
1770        );
1771    }
1772
1773    #[cfg(windows)]
1774    #[test]
1775    fn from_os_str_invalid_utf8_borrowed_rejected() {
1776        use std::os::windows::ffi::OsStringExt;
1777        let input = OsString::from_wide(&[0x68, 0xD800, 0x69]);
1778        assert_eq!(
1779            *PathElementCS::from_os_str(input.as_os_str())
1780                .unwrap_err()
1781                .kind(),
1782            ErrorKind::InvalidUtf8
1783        );
1784    }
1785
1786    #[cfg(windows)]
1787    #[test]
1788    fn from_os_str_invalid_utf8_owned_rejected() {
1789        use std::os::windows::ffi::OsStringExt;
1790        let input = OsString::from_wide(&[0x68, 0xD800, 0x69]);
1791        assert_eq!(
1792            *PathElementCS::from_os_str(input).unwrap_err().kind(),
1793            ErrorKind::InvalidUtf8
1794        );
1795    }
1796
1797    // CI "H\tllo": original="H\tllo", os_compatible="H␉llo", normalized="h␉llo"
1798    // All three differ, so asserting "H\u{2409}llo" proves it's os_compatible.
1799    #[test]
1800    fn os_str_returns_os_compatible() {
1801        let pe = PathElementCI::new("H\tllo").unwrap();
1802        assert_eq!(pe.os_str(), OsStr::new("H\u{2409}llo"));
1803    }
1804
1805    #[test]
1806    fn into_os_str_returns_os_compatible() {
1807        let pe = PathElementCI::new("H\tllo").unwrap();
1808        let result = pe.into_os_str();
1809        assert_eq!(result, OsStr::new("H\u{2409}llo"));
1810    }
1811
1812    #[test]
1813    fn into_os_str_borrows_when_no_transformation() {
1814        let input = OsStr::new("hello.txt");
1815        let pe = PathElementCS::from_os_str(input).unwrap();
1816        let result = pe.into_os_str();
1817        assert!(matches!(result, Cow::Borrowed(_)));
1818        assert_eq!(result, OsStr::new("hello.txt"));
1819    }
1820
1821    #[test]
1822    fn into_os_str_ci_borrows_when_already_folded() {
1823        let input = OsStr::new("hello.txt");
1824        let pe = PathElementCI::from_os_str(input).unwrap();
1825        let result = pe.into_os_str();
1826        assert!(matches!(result, Cow::Borrowed(_)));
1827        assert_eq!(result, OsStr::new("hello.txt"));
1828    }
1829
1830    // Borrowed input, but NFC normalization produces owned output.
1831    // On Apple, the os-compatible form is NFD, which matches the NFD input, so it borrows.
1832    #[test]
1833    fn into_os_str_owned_when_nfc_transforms() {
1834        let input = OsStr::new("e\u{0301}.txt"); // NFD
1835        let pe = PathElementCS::from_os_str(input).unwrap();
1836        let result = pe.into_os_str();
1837        #[cfg(target_vendor = "apple")]
1838        assert!(matches!(result, Cow::Borrowed(_)));
1839        #[cfg(not(target_vendor = "apple"))]
1840        assert!(matches!(result, Cow::Owned(_)));
1841    }
1842
1843    // NFC input borrows on non-Apple (os-compatible is NFC), owned on Apple (os-compatible is NFD).
1844    #[test]
1845    fn into_os_str_owned_when_nfd_transforms() {
1846        let input = OsStr::new("\u{00E9}.txt"); // NFC
1847        let pe = PathElementCS::from_os_str(input).unwrap();
1848        let result = pe.into_os_str();
1849        #[cfg(target_vendor = "apple")]
1850        assert!(matches!(result, Cow::Owned(_)));
1851        #[cfg(not(target_vendor = "apple"))]
1852        assert!(matches!(result, Cow::Borrowed(_)));
1853    }
1854
1855    #[test]
1856    fn from_os_str_cs_matches_typed() {
1857        let input = OsStr::new("Hello.txt");
1858        let dyn_pe = PathElement::from_os_str_cs(input).unwrap();
1859        let cs_pe = PathElementCS::from_os_str(input).unwrap();
1860        assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
1861        assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
1862        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
1863    }
1864
1865    #[test]
1866    fn from_os_str_ci_matches_typed() {
1867        let input = OsStr::new("Hello.txt");
1868        let dyn_pe = PathElement::from_os_str_ci(input).unwrap();
1869        let ci_pe = PathElementCI::from_os_str(input).unwrap();
1870        assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
1871        assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
1872        assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
1873    }
1874
1875    #[test]
1876    fn from_os_str_dynamic_case_sensitivity() {
1877        let input = OsStr::new("Hello.txt");
1878        let pe = PathElement::from_os_str(input, CaseInsensitive).unwrap();
1879        assert_eq!(pe.normalized(), "hello.txt");
1880    }
1881}