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