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