use alloc::borrow::Cow;
use alloc::string::String;
#[cfg(feature = "std")]
use std::ffi::{OsStr, OsString};
use crate::Result;
use crate::case_sensitivity::{CaseInsensitive, CaseSensitive, CaseSensitivity};
use crate::error::{Error, ErrorKind};
use crate::normalize::{normalize_ci_from_normalized_cs, normalize_cs};
use crate::os::os_compatible_from_normalized_cs;
use crate::utils::SubstringOrOwned;
pub type PathElementCS<'a> = PathElementGeneric<'a, CaseSensitive>;
pub type PathElementCI<'a> = PathElementGeneric<'a, CaseInsensitive>;
pub type PathElement<'a> = PathElementGeneric<'a, CaseSensitivity>;
#[derive(Clone)]
pub struct PathElementGeneric<'a, S> {
original: Cow<'a, str>,
normalized: SubstringOrOwned,
os_compatible: SubstringOrOwned,
case_sensitivity: S,
}
impl<S: core::fmt::Debug> core::fmt::Debug for PathElementGeneric<'_, S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PathElement")
.field("original", &self.original())
.field("normalized", &self.normalized())
.field("os_compatible", &self.os_compatible())
.field("case_sensitivity", &self.case_sensitivity)
.finish()
}
}
impl<'a, S1, S2> PartialEq<PathElementGeneric<'a, S2>> for PathElementGeneric<'_, S1>
where
for<'s> CaseSensitivity: From<&'s S1> + From<&'s S2>,
{
fn eq(&self, other: &PathElementGeneric<'a, S2>) -> bool {
assert_eq!(
CaseSensitivity::from(&self.case_sensitivity),
CaseSensitivity::from(&other.case_sensitivity),
"comparing PathElements with different case sensitivity"
);
self.normalized() == other.normalized()
}
}
impl<S> Eq for PathElementGeneric<'_, S> where for<'s> CaseSensitivity: From<&'s S> {}
impl<'a, S1, S2> PartialOrd<PathElementGeneric<'a, S2>> for PathElementGeneric<'_, S1>
where
for<'s> CaseSensitivity: From<&'s S1> + From<&'s S2>,
{
fn partial_cmp(&self, other: &PathElementGeneric<'a, S2>) -> Option<core::cmp::Ordering> {
if CaseSensitivity::from(&self.case_sensitivity)
!= CaseSensitivity::from(&other.case_sensitivity)
{
return None;
}
Some(self.normalized().cmp(other.normalized()))
}
}
impl<S> Ord for PathElementGeneric<'_, S>
where
for<'s> CaseSensitivity: From<&'s S>,
{
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
assert_eq!(
CaseSensitivity::from(&self.case_sensitivity),
CaseSensitivity::from(&other.case_sensitivity),
"comparing PathElements with different case sensitivity"
);
self.normalized().cmp(other.normalized())
}
}
impl core::hash::Hash for PathElementCS<'_> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.normalized().hash(state);
}
}
impl core::hash::Hash for PathElementCI<'_> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.normalized().hash(state);
}
}
impl<'a> PathElementCS<'a> {
pub fn new(original: impl Into<Cow<'a, str>>) -> Result<Self> {
Self::with_case_sensitivity(original, CaseSensitive)
}
pub fn from_bytes(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
Self::from_bytes_with_case_sensitivity(original, CaseSensitive)
}
#[cfg(feature = "std")]
pub fn from_os_str(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
Self::from_os_str_with_case_sensitivity(original, CaseSensitive)
}
}
impl<'a> PathElementCI<'a> {
pub fn new(original: impl Into<Cow<'a, str>>) -> Result<Self> {
Self::with_case_sensitivity(original, CaseInsensitive)
}
pub fn from_bytes(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
Self::from_bytes_with_case_sensitivity(original, CaseInsensitive)
}
#[cfg(feature = "std")]
pub fn from_os_str(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
Self::from_os_str_with_case_sensitivity(original, CaseInsensitive)
}
}
impl<'a> PathElementGeneric<'a, CaseSensitivity> {
pub fn new(
original: impl Into<Cow<'a, str>>,
case_sensitivity: impl Into<CaseSensitivity>,
) -> Result<Self> {
Self::with_case_sensitivity(original, case_sensitivity)
}
pub fn from_bytes(
original: impl Into<Cow<'a, [u8]>>,
case_sensitivity: impl Into<CaseSensitivity>,
) -> Result<Self> {
Self::from_bytes_with_case_sensitivity(original, case_sensitivity)
}
#[cfg(feature = "std")]
pub fn from_os_str(
original: impl Into<Cow<'a, OsStr>>,
case_sensitivity: impl Into<CaseSensitivity>,
) -> Result<Self> {
Self::from_os_str_with_case_sensitivity(original, case_sensitivity)
}
pub fn new_cs(original: impl Into<Cow<'a, str>>) -> Result<Self> {
Self::with_case_sensitivity(original, CaseSensitive)
}
pub fn new_ci(original: impl Into<Cow<'a, str>>) -> Result<Self> {
Self::with_case_sensitivity(original, CaseInsensitive)
}
pub fn from_bytes_cs(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
Self::from_bytes_with_case_sensitivity(original, CaseSensitive)
}
pub fn from_bytes_ci(original: impl Into<Cow<'a, [u8]>>) -> Result<Self> {
Self::from_bytes_with_case_sensitivity(original, CaseInsensitive)
}
#[cfg(feature = "std")]
pub fn from_os_str_cs(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
Self::from_os_str_with_case_sensitivity(original, CaseSensitive)
}
#[cfg(feature = "std")]
pub fn from_os_str_ci(original: impl Into<Cow<'a, OsStr>>) -> Result<Self> {
Self::from_os_str_with_case_sensitivity(original, CaseInsensitive)
}
}
impl<'a, S> PathElementGeneric<'a, S>
where
for<'s> CaseSensitivity: From<&'s S>,
{
pub fn from_bytes_with_case_sensitivity(
original: impl Into<Cow<'a, [u8]>>,
case_sensitivity: impl Into<S>,
) -> Result<Self> {
let utf8_err =
|bytes: &[u8]| Error::new(ErrorKind::InvalidUtf8, String::from_utf8_lossy(bytes));
let cow_str: Cow<'a, str> = match original.into() {
Cow::Borrowed(b) => Cow::Borrowed(core::str::from_utf8(b).map_err(|_| utf8_err(b))?),
Cow::Owned(v) => Cow::Owned(String::from_utf8(v).map_err(|e| utf8_err(e.as_bytes()))?),
};
Self::with_case_sensitivity(cow_str, case_sensitivity)
}
#[cfg(feature = "std")]
pub fn from_os_str_with_case_sensitivity(
original: impl Into<Cow<'a, OsStr>>,
case_sensitivity: impl Into<S>,
) -> Result<Self> {
let cow_bytes: Cow<'a, [u8]> = match original.into() {
Cow::Borrowed(os) => Cow::Borrowed(os.as_encoded_bytes()),
Cow::Owned(os) => Cow::Owned(os.into_encoded_bytes()),
};
Self::from_bytes_with_case_sensitivity(cow_bytes, case_sensitivity)
}
pub fn with_case_sensitivity(
original: impl Into<Cow<'a, str>>,
case_sensitivity: impl Into<S>,
) -> Result<Self> {
let original = original.into();
let case_sensitivity = case_sensitivity.into();
let cs = CaseSensitivity::from(&case_sensitivity);
let cs_normalized = match normalize_cs(&original) {
Ok(v) => v,
Err(kind) => return Err(Error::new(kind, original)),
};
let normalized = match cs {
CaseSensitivity::Sensitive => SubstringOrOwned::new(&cs_normalized, &original),
CaseSensitivity::Insensitive => {
SubstringOrOwned::new(&normalize_ci_from_normalized_cs(&cs_normalized), &original)
}
};
let os_str = match os_compatible_from_normalized_cs(&cs_normalized) {
Ok(v) => v,
Err(kind) => return Err(Error::new(kind, original)),
};
let os_compatible = SubstringOrOwned::new(&os_str, &original);
Ok(Self {
original,
normalized,
os_compatible,
case_sensitivity,
})
}
pub fn case_sensitivity(&self) -> CaseSensitivity {
CaseSensitivity::from(&self.case_sensitivity)
}
}
impl<'a, S> PathElementGeneric<'a, S> {
pub fn original(&self) -> &str {
&self.original
}
pub fn into_original(self) -> Cow<'a, str> {
self.original
}
pub fn is_normalized(&self) -> bool {
self.normalized.is_identity(&self.original)
}
pub fn normalized(&self) -> &str {
self.normalized.as_ref(&self.original)
}
pub fn into_normalized(self) -> Cow<'a, str> {
self.normalized.into_cow(self.original)
}
pub fn is_os_compatible(&self) -> bool {
self.os_compatible.is_identity(&self.original)
}
pub fn os_compatible(&self) -> &str {
self.os_compatible.as_ref(&self.original)
}
pub fn into_os_compatible(self) -> Cow<'a, str> {
self.os_compatible.into_cow(self.original)
}
#[cfg(feature = "std")]
pub fn os_str(&self) -> &OsStr {
OsStr::new(self.os_compatible())
}
#[cfg(feature = "std")]
pub fn into_os_str(self) -> Cow<'a, OsStr> {
match self.into_os_compatible() {
Cow::Borrowed(s) => Cow::Borrowed(OsStr::new(s)),
Cow::Owned(s) => Cow::Owned(OsString::from(s)),
}
}
pub fn is_borrowed(&self) -> bool {
matches!(self.original, Cow::Borrowed(_))
}
pub fn is_owned(&self) -> bool {
matches!(self.original, Cow::Owned(_))
}
pub fn into_owned(self) -> PathElementGeneric<'static, S> {
PathElementGeneric {
original: Cow::Owned(self.original.into_owned()),
normalized: self.normalized,
os_compatible: self.os_compatible,
case_sensitivity: self.case_sensitivity,
}
}
}
impl<'a> From<PathElementCS<'a>> for PathElement<'a> {
fn from(pe: PathElementCS<'a>) -> Self {
Self {
original: pe.original,
normalized: pe.normalized,
os_compatible: pe.os_compatible,
case_sensitivity: CaseSensitivity::Sensitive,
}
}
}
impl<'a> From<PathElementCI<'a>> for PathElement<'a> {
fn from(pe: PathElementCI<'a>) -> Self {
Self {
original: pe.original,
normalized: pe.normalized,
os_compatible: pe.os_compatible,
case_sensitivity: CaseSensitivity::Insensitive,
}
}
}
impl<'a> TryFrom<PathElement<'a>> for PathElementCS<'a> {
type Error = PathElementCI<'a>;
fn try_from(pe: PathElement<'a>) -> core::result::Result<Self, Self::Error> {
if pe.case_sensitivity == CaseSensitivity::Sensitive {
Ok(Self {
original: pe.original,
normalized: pe.normalized,
os_compatible: pe.os_compatible,
case_sensitivity: CaseSensitive,
})
} else {
Err(Self::Error {
original: pe.original,
normalized: pe.normalized,
os_compatible: pe.os_compatible,
case_sensitivity: CaseInsensitive,
})
}
}
}
impl<'a> TryFrom<PathElement<'a>> for PathElementCI<'a> {
type Error = PathElementCS<'a>;
fn try_from(pe: PathElement<'a>) -> core::result::Result<Self, Self::Error> {
if pe.case_sensitivity == CaseSensitivity::Insensitive {
Ok(Self {
original: pe.original,
normalized: pe.normalized,
os_compatible: pe.os_compatible,
case_sensitivity: CaseInsensitive,
})
} else {
Err(Self::Error {
original: pe.original,
normalized: pe.normalized,
os_compatible: pe.os_compatible,
case_sensitivity: CaseSensitive,
})
}
}
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use wasm_bindgen_test::wasm_bindgen_test as test;
use super::{PathElement, PathElementCI, PathElementCS};
use crate::ErrorKind;
use crate::case_sensitivity::{CaseInsensitive, CaseSensitive, CaseSensitivity};
use crate::normalize::{normalize_ci_from_normalized_cs, normalize_cs};
use crate::os::os_compatible_from_normalized_cs;
#[test]
fn path_element_cs_matches_freestanding() {
let input = "Hello";
let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
assert_eq!(pe.original(), input);
assert_eq!(pe.normalized(), normalize_cs(input).unwrap().as_ref());
assert_eq!(
pe.os_compatible(),
os_compatible_from_normalized_cs(&normalize_cs(input).unwrap())
.unwrap()
.as_ref()
);
}
#[test]
fn path_element_ci_matches_freestanding() {
let input = "Hello";
let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
assert_eq!(pe.original(), "Hello");
assert_eq!(pe.normalized(), "hello");
}
#[test]
fn path_element_cs_os_compatible_platform_dependent() {
let input = "nul.e\u{0301}";
let pe = PathElementCS::new(input).unwrap();
assert_eq!(pe.original(), "nul.e\u{0301}");
assert_eq!(pe.normalized(), "nul.\u{00E9}");
#[cfg(target_os = "windows")]
assert_eq!(pe.os_compatible(), "\u{FF4E}ul.\u{00E9}");
#[cfg(target_vendor = "apple")]
assert_eq!(pe.os_compatible(), "nul.e\u{0301}");
#[cfg(not(any(target_os = "windows", target_vendor = "apple")))]
assert_eq!(pe.os_compatible(), "nul.\u{00E9}");
}
#[test]
fn path_element_cs_nfc_matches_freestanding() {
let input = "e\u{0301}.txt";
let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
assert_eq!(pe.normalized(), normalize_cs(input).unwrap().as_ref());
}
#[test]
fn path_element_ci_casefold_matches_freestanding() {
let input = "Hello.txt";
let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
let cs = normalize_cs(input).unwrap();
assert_eq!(
pe.normalized(),
normalize_ci_from_normalized_cs(&cs).as_ref()
);
}
#[test]
fn path_element_cs_normalized_borrows_from_original() {
let input = "hello.txt";
let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
assert_eq!(pe.normalized(), "hello.txt");
assert!(core::ptr::eq(pe.normalized().as_ptr(), input.as_ptr()));
}
#[test]
fn path_element_cs_into_normalized_borrows() {
let input = "hello.txt";
let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
let norm = pe.into_normalized();
assert!(matches!(norm, Cow::Borrowed(_)));
assert_eq!(norm, "hello.txt");
}
#[test]
fn path_element_cs_into_os_compatible_borrows() {
let input = "hello.txt";
let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
let pres = pe.into_os_compatible();
assert!(matches!(pres, Cow::Borrowed(_)));
assert_eq!(pres.as_ref(), "hello.txt");
}
#[test]
fn path_element_ci_normalized_borrows_when_already_folded() {
let input = "hello.txt";
let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
assert!(core::ptr::eq(pe.normalized().as_ptr(), input.as_ptr()));
}
#[test]
fn path_element_ci_into_normalized_borrows_when_already_folded() {
let input = "hello.txt";
let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
let norm = pe.into_normalized();
assert!(matches!(norm, Cow::Borrowed(_)));
assert_eq!(norm, "hello.txt");
}
#[test]
fn path_element_ci_into_os_compatible_borrows_when_already_folded() {
let input = "hello.txt";
let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
let pres = pe.into_os_compatible();
assert!(matches!(pres, Cow::Borrowed(_)));
assert_eq!(pres.as_ref(), "hello.txt");
}
#[test]
fn path_element_cs_trimmed_borrows_suffix() {
let input = " hello.txt";
let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
assert_eq!(pe.normalized(), "hello.txt");
assert!(core::ptr::eq(pe.normalized().as_ptr(), input[3..].as_ptr()));
}
#[test]
fn path_element_into_original_returns_original() {
let input = " Hello.txt ";
let pe = PathElementCS::new(Cow::Borrowed(input)).unwrap();
let orig = pe.into_original();
assert!(matches!(orig, Cow::Borrowed(_)));
assert_eq!(orig, input);
}
#[test]
fn path_element_is_normalized_when_unchanged() {
let pe = PathElementCS::new("hello.txt").unwrap();
assert!(pe.is_normalized());
}
#[test]
fn path_element_is_not_normalized_when_trimmed() {
let pe = PathElementCS::new(" hello.txt ").unwrap();
assert!(!pe.is_normalized());
}
#[test]
fn path_element_is_not_normalized_when_trailing_whitespace_trimmed() {
let pe = PathElementCS::new("hello.txt ").unwrap();
assert!(!pe.is_normalized());
assert_eq!(pe.normalized(), "hello.txt");
}
#[test]
fn path_element_is_not_normalized_when_casefolded() {
let pe = PathElementCI::new("Hello.txt").unwrap();
assert!(!pe.is_normalized());
}
#[test]
fn path_element_ci_is_normalized_when_already_folded() {
let pe = PathElementCI::new("hello.txt").unwrap();
assert!(pe.is_normalized());
}
#[test]
fn path_element_is_os_compatible_ascii() {
let pe = PathElementCS::new("hello.txt").unwrap();
assert!(pe.is_os_compatible());
}
#[test]
fn path_element_is_not_os_compatible_trailing_whitespace_ci() {
let pe = PathElementCI::new("hello.txt ").unwrap();
assert!(!pe.is_os_compatible());
}
#[test]
fn path_element_is_not_os_compatible_trailing_whitespace_cs() {
let pe = PathElementCS::new("hello.txt ").unwrap();
assert!(!pe.is_os_compatible());
}
#[test]
fn path_element_is_os_compatible_nfc_input() {
let pe = PathElementCS::new("\u{00E9}.txt").unwrap();
#[cfg(target_vendor = "apple")]
assert!(!pe.is_os_compatible());
#[cfg(not(target_vendor = "apple"))]
assert!(pe.is_os_compatible());
}
#[test]
fn path_element_is_os_compatible_nfd_input() {
let pe = PathElementCS::new("e\u{0301}.txt").unwrap();
#[cfg(target_vendor = "apple")]
assert!(pe.is_os_compatible());
#[cfg(not(target_vendor = "apple"))]
assert!(!pe.is_os_compatible());
}
#[test]
fn path_element_is_os_compatible_nfd_input_ci() {
let pe = PathElementCI::new("e\u{0301}.txt").unwrap();
#[cfg(target_vendor = "apple")]
assert!(pe.is_os_compatible());
#[cfg(not(target_vendor = "apple"))]
assert!(!pe.is_os_compatible());
}
#[test]
fn path_element_is_os_compatible_nfc_input_ci() {
let pe = PathElementCI::new("\u{00E9}.txt").unwrap();
#[cfg(target_vendor = "apple")]
assert!(!pe.is_os_compatible());
#[cfg(not(target_vendor = "apple"))]
assert!(pe.is_os_compatible());
}
#[test]
fn path_element_is_not_os_compatible_reserved_on_windows() {
let pe = PathElementCS::new("nul.txt").unwrap();
#[cfg(target_os = "windows")]
assert!(!pe.is_os_compatible());
#[cfg(not(target_os = "windows"))]
assert!(pe.is_os_compatible());
}
#[test]
fn path_element_borrowed_is_borrowed() {
let pe = PathElementCS::new(Cow::Borrowed("hello.txt")).unwrap();
assert!(pe.is_borrowed());
assert!(!pe.is_owned());
}
#[test]
fn path_element_owned_is_owned() {
let pe = PathElementCS::new(Cow::Owned("hello.txt".to_string())).unwrap();
assert!(pe.is_owned());
assert!(!pe.is_borrowed());
}
#[test]
fn path_element_into_owned_is_owned() {
let pe = PathElementCS::new(Cow::Borrowed("hello.txt")).unwrap();
let owned = pe.into_owned();
assert!(owned.is_owned());
}
#[test]
fn path_element_into_owned_preserves_values() {
let input = "Hello";
let pe = PathElementCI::new(Cow::Borrowed(input)).unwrap();
let owned = pe.into_owned();
assert_eq!(owned.original(), "Hello");
assert_eq!(owned.normalized(), "hello");
}
#[test]
fn new_rejects_empty() {
assert_eq!(PathElementCS::new("").unwrap_err().kind, ErrorKind::Empty);
}
#[test]
fn new_rejects_dot() {
assert_eq!(
PathElementCS::new(".").unwrap_err().kind,
ErrorKind::CurrentDirectoryMarker
);
}
#[test]
fn new_rejects_dotdot() {
assert_eq!(
PathElementCS::new("..").unwrap_err().kind,
ErrorKind::ParentDirectoryMarker
);
}
#[test]
fn new_rejects_slash() {
assert_eq!(
PathElementCS::new("a/b").unwrap_err().kind,
ErrorKind::ContainsForwardSlash
);
}
#[test]
fn new_rejects_null() {
assert_eq!(
PathElementCS::new("\0").unwrap_err().kind,
ErrorKind::ContainsNullByte
);
}
#[test]
fn new_rejects_unassigned() {
assert_eq!(
PathElementCS::new("\u{0378}").unwrap_err().kind,
ErrorKind::ContainsUnassignedChar
);
}
#[test]
fn path_element_eq_same_cs() {
let a = PathElementCS::new("hello.txt").unwrap();
let b = PathElementCS::new("hello.txt").unwrap();
assert_eq!(a, b);
}
#[test]
fn path_element_eq_different_original_same_normalized_cs() {
let a = PathElementCS::new(" hello.txt ").unwrap();
let b = PathElementCS::new("hello.txt").unwrap();
assert_ne!(a.original(), b.original());
assert_eq!(a, b);
}
#[test]
fn path_element_ne_different_case_cs() {
let a = PathElementCS::new("Hello.txt").unwrap();
let b = PathElementCS::new("hello.txt").unwrap();
assert_ne!(a, b);
}
#[test]
fn path_element_eq_different_case_ci() {
let a = PathElementCI::new("Hello.txt").unwrap();
let b = PathElementCI::new("hello.txt").unwrap();
assert_eq!(a, b);
}
#[test]
fn path_element_eq_nfc_nfd_cs() {
let a = PathElementCS::new("\u{00E9}.txt").unwrap();
let b = PathElementCS::new("e\u{0301}.txt").unwrap();
assert_eq!(a, b);
}
#[test]
fn path_element_eq_cross_lifetime() {
let owned = PathElementCS::new("hello.txt").unwrap().into_owned();
let input = "hello.txt";
let borrowed = PathElementCS::new(Cow::Borrowed(input)).unwrap();
assert_eq!(owned, borrowed);
assert_eq!(borrowed, owned);
}
#[test]
#[should_panic(expected = "different case sensitivity")]
fn path_element_eq_panics_on_mixed_dynamic_case_sensitivity() {
let a = PathElement::new("hello", CaseSensitive).unwrap();
let b = PathElement::new("hello", CaseInsensitive).unwrap();
let _ = a == b;
}
#[test]
fn path_element_ord_alphabetical_cs() {
let a = PathElementCS::new("apple").unwrap();
let b = PathElementCS::new("banana").unwrap();
assert!(a < b);
assert!(b > a);
}
#[test]
fn path_element_ord_equal_cs() {
let a = PathElementCS::new("hello").unwrap();
let b = PathElementCS::new("hello").unwrap();
assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
}
#[test]
fn path_element_ord_case_ci() {
let a = PathElementCI::new("Apple").unwrap();
let b = PathElementCI::new("apple").unwrap();
assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
}
#[test]
fn path_element_partial_ord_cross_lifetime() {
let owned = PathElementCS::new("apple").unwrap().into_owned();
let input = "banana";
let borrowed = PathElementCS::new(Cow::Borrowed(input)).unwrap();
assert!(owned < borrowed);
}
#[test]
fn path_element_partial_ord_none_on_mixed_dynamic_case_sensitivity() {
let a = PathElement::new("hello", CaseSensitive).unwrap();
let b = PathElement::new("hello", CaseInsensitive).unwrap();
assert_eq!(a.partial_cmp(&b), None);
}
#[test]
fn path_element_ord_sortable() {
let mut elems: Vec<_> = ["cherry", "apple", "banana"]
.iter()
.map(|s| PathElementCS::new(Cow::Borrowed(*s)).unwrap())
.collect();
elems.sort();
let names: Vec<_> = elems.iter().map(PathElementCS::normalized).collect();
assert_eq!(names, &["apple", "banana", "cherry"]);
}
#[test]
fn path_element_ord_ci_sortable() {
let mut elems: Vec<_> = ["Cherry", "apple", "BANANA"]
.iter()
.map(|s| PathElementCI::new(Cow::Borrowed(*s)).unwrap())
.collect();
elems.sort();
let names: Vec<_> = elems.iter().map(PathElementCI::normalized).collect();
assert_eq!(names, &["apple", "banana", "cherry"]);
}
#[test]
fn from_cs_into_dynamic() {
let pe = PathElementCS::new("hello").unwrap();
let dyn_pe: PathElement<'_> = pe.into();
assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
assert_eq!(dyn_pe.normalized(), "hello");
}
#[test]
fn from_ci_into_dynamic() {
let pe = PathElementCI::new("Hello").unwrap();
let dyn_pe: PathElement<'_> = pe.into();
assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
assert_eq!(dyn_pe.normalized(), "hello");
}
#[test]
fn try_from_dynamic_to_cs() {
let pe = PathElement::new("hello", CaseSensitive).unwrap();
let cs_pe: PathElementCS<'_> = pe.try_into().unwrap();
assert_eq!(cs_pe.normalized(), "hello");
}
#[test]
fn try_from_dynamic_to_cs_wrong_variant() {
let pe = PathElement::new("Hello", CaseInsensitive).unwrap();
let err: PathElementCI<'_> = PathElementCS::try_from(pe).unwrap_err();
assert_eq!(err.original, "Hello");
assert_eq!(err.normalized(), "hello");
assert_eq!(err.os_compatible(), "Hello");
}
#[test]
fn try_from_dynamic_to_ci() {
let pe = PathElement::new("Hello", CaseInsensitive).unwrap();
let ci_pe: PathElementCI<'_> = pe.try_into().unwrap();
assert_eq!(ci_pe.normalized(), "hello");
}
#[test]
fn dyn_new_cs() {
let pe = PathElement::new_cs("Hello.txt").unwrap();
assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
assert_eq!(pe.normalized(), "Hello.txt");
}
#[test]
fn dyn_new_ci() {
let pe = PathElement::new_ci("Hello.txt").unwrap();
assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
assert_eq!(pe.normalized(), "hello.txt");
}
#[test]
fn dyn_new_cs_matches_typed() {
let dyn_pe = PathElement::new_cs("Hello.txt").unwrap();
let cs_pe = PathElementCS::new("Hello.txt").unwrap();
assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
}
#[test]
fn dyn_new_ci_matches_typed() {
let dyn_pe = PathElement::new_ci("Hello.txt").unwrap();
let ci_pe = PathElementCI::new("Hello.txt").unwrap();
assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
}
#[test]
fn case_sensitivity_cs() {
let pe = PathElementCS::new("hello").unwrap();
assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
}
#[test]
fn case_sensitivity_ci() {
let pe = PathElementCI::new("hello").unwrap();
assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
}
#[test]
fn case_sensitivity_dyn() {
let cs = PathElement::new("hello", CaseSensitive).unwrap();
let ci = PathElement::new("hello", CaseInsensitive).unwrap();
assert_eq!(cs.case_sensitivity(), CaseSensitivity::Sensitive);
assert_eq!(ci.case_sensitivity(), CaseSensitivity::Insensitive);
}
#[test]
fn partial_ord_dyn_same_case_sensitivity() {
let a = PathElement::new("apple", CaseSensitive).unwrap();
let b = PathElement::new("banana", CaseSensitive).unwrap();
assert!(a < b);
}
#[test]
fn partial_ord_dyn_none_on_mismatch() {
let a = PathElement::new("hello", CaseSensitive).unwrap();
let b = PathElement::new("hello", CaseInsensitive).unwrap();
assert_eq!(a.partial_cmp(&b), None);
}
#[test]
fn try_from_dynamic_to_ci_wrong_variant() {
let pe = PathElement::new("Hello", CaseSensitive).unwrap();
let err: PathElementCS<'_> = PathElementCI::try_from(pe).unwrap_err();
assert_eq!(err.original, "Hello");
assert_eq!(err.normalized(), "Hello");
assert_eq!(err.os_compatible(), "Hello");
}
#[test]
fn into_owned_preserves_cs_case_sensitivity() {
let pe = PathElementCS::new("hello").unwrap();
let owned = pe.into_owned();
assert_eq!(owned.case_sensitivity(), CaseSensitivity::Sensitive);
}
#[test]
fn into_owned_preserves_dyn_case_sensitivity() {
let pe = PathElement::new("hello", CaseInsensitive).unwrap();
let owned = pe.into_owned();
assert_eq!(owned.case_sensitivity(), CaseSensitivity::Insensitive);
}
#[test]
fn eq_cs_vs_dyn_same_case_sensitivity() {
let cs = PathElementCS::new("hello").unwrap();
let dyn_cs = PathElement::new_cs("hello").unwrap();
assert_eq!(cs, dyn_cs);
assert_eq!(dyn_cs, cs);
}
#[test]
fn eq_ci_vs_dyn_same_case_sensitivity() {
let ci = PathElementCI::new("Hello").unwrap();
let dyn_ci = PathElement::new_ci("hello").unwrap();
assert_eq!(ci, dyn_ci);
assert_eq!(dyn_ci, ci);
}
#[test]
#[should_panic(expected = "different case sensitivity")]
fn eq_cs_vs_ci_panics() {
let cs = PathElementCS::new("hello").unwrap();
let ci = PathElementCI::new("hello").unwrap();
let _ = cs == ci;
}
#[test]
#[should_panic(expected = "different case sensitivity")]
fn eq_cs_vs_dyn_ci_panics() {
let cs = PathElementCS::new("hello").unwrap();
let dyn_ci = PathElement::new_ci("hello").unwrap();
let _ = cs == dyn_ci;
}
#[test]
fn partial_ord_cs_vs_dyn_same_case_sensitivity() {
let cs = PathElementCS::new("apple").unwrap();
let dyn_cs = PathElement::new_cs("banana").unwrap();
assert!(cs < dyn_cs);
assert!(dyn_cs > cs);
}
#[test]
fn partial_ord_cs_vs_ci_none() {
let cs = PathElementCS::new("hello").unwrap();
let ci = PathElementCI::new("hello").unwrap();
assert_eq!(cs.partial_cmp(&ci), None);
assert_eq!(ci.partial_cmp(&cs), None);
}
#[test]
fn partial_ord_cs_vs_dyn_ci_none() {
let cs = PathElementCS::new("hello").unwrap();
let dyn_ci = PathElement::new_ci("hello").unwrap();
assert_eq!(cs.partial_cmp(&dyn_ci), None);
assert_eq!(dyn_ci.partial_cmp(&cs), None);
}
#[test]
fn from_bytes_cs_borrowed_matches_new() {
let pe_bytes = PathElementCS::from_bytes(b"hello.txt" as &[u8]).unwrap();
let pe_str = PathElementCS::new("hello.txt").unwrap();
assert_eq!(pe_bytes.original(), pe_str.original());
assert_eq!(pe_bytes.normalized(), pe_str.normalized());
assert_eq!(pe_bytes.os_compatible(), pe_str.os_compatible());
}
#[test]
fn from_bytes_ci_borrowed_matches_new() {
let pe_bytes = PathElementCI::from_bytes(b"Hello.txt" as &[u8]).unwrap();
let pe_str = PathElementCI::new("Hello.txt").unwrap();
assert_eq!(pe_bytes.original(), pe_str.original());
assert_eq!(pe_bytes.normalized(), pe_str.normalized());
assert_eq!(pe_bytes.os_compatible(), pe_str.os_compatible());
}
#[test]
fn from_bytes_owned_matches_new() {
let pe_bytes = PathElementCS::from_bytes(b"hello.txt".to_vec()).unwrap();
let pe_str = PathElementCS::new("hello.txt").unwrap();
assert_eq!(pe_bytes.original(), pe_str.original());
assert_eq!(pe_bytes.normalized(), pe_str.normalized());
}
#[test]
fn from_bytes_borrowed_preserves_borrow() {
let input: &[u8] = b"hello.txt";
let pe = PathElementCS::from_bytes(input).unwrap();
let orig = pe.into_original();
assert!(matches!(orig, Cow::Borrowed(_)));
}
#[test]
fn from_bytes_owned_is_owned() {
let pe = PathElementCS::from_bytes(b"hello.txt".to_vec()).unwrap();
assert!(pe.is_owned());
}
#[test]
fn from_bytes_invalid_utf8_borrowed_rejected() {
let input: &[u8] = &[0x68, 0x69, 0xFF]; let err = PathElementCS::from_bytes(input).unwrap_err();
assert_eq!(err.kind, ErrorKind::InvalidUtf8);
}
#[test]
fn from_bytes_invalid_utf8_owned_rejected() {
let input = vec![0x68, 0x69, 0xFF];
let err = PathElementCS::from_bytes(input).unwrap_err();
assert_eq!(err.kind, ErrorKind::InvalidUtf8);
}
#[test]
fn from_bytes_dynamic_case_sensitivity() {
let pe = PathElement::from_bytes(b"Hello.txt" as &[u8], CaseInsensitive).unwrap();
assert_eq!(pe.normalized(), "hello.txt");
assert_eq!(pe.case_sensitivity(), CaseSensitivity::Insensitive);
}
#[test]
fn from_bytes_cs_matches_typed() {
let input: &[u8] = b"Hello.txt";
let dyn_pe = PathElement::from_bytes_cs(input).unwrap();
let cs_pe = PathElementCS::from_bytes(input).unwrap();
assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
}
#[test]
fn from_bytes_ci_matches_typed() {
let input: &[u8] = b"Hello.txt";
let dyn_pe = PathElement::from_bytes_ci(input).unwrap();
let ci_pe = PathElementCI::from_bytes(input).unwrap();
assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
}
#[test]
fn from_bytes_with_case_sensitivity_cs() {
let pe = PathElementCS::from_bytes(b"Hello.txt" as &[u8]).unwrap();
assert_eq!(pe.normalized(), "Hello.txt");
}
#[test]
fn from_bytes_with_case_sensitivity_ci() {
let pe = PathElementCI::from_bytes(b"Hello.txt" as &[u8]).unwrap();
assert_eq!(pe.normalized(), "hello.txt");
}
#[test]
fn from_bytes_rejects_empty() {
assert_eq!(
PathElementCS::from_bytes(b"" as &[u8]).unwrap_err().kind,
ErrorKind::Empty
);
}
#[test]
fn from_bytes_rejects_dot() {
assert_eq!(
PathElementCS::from_bytes(b"." as &[u8]).unwrap_err().kind,
ErrorKind::CurrentDirectoryMarker
);
}
#[test]
fn from_bytes_rejects_dotdot() {
assert_eq!(
PathElementCS::from_bytes(b".." as &[u8]).unwrap_err().kind,
ErrorKind::ParentDirectoryMarker
);
}
#[test]
fn from_bytes_rejects_slash() {
assert_eq!(
PathElementCS::from_bytes(b"a/b" as &[u8]).unwrap_err().kind,
ErrorKind::ContainsForwardSlash
);
}
#[test]
fn from_bytes_rejects_null() {
assert_eq!(
PathElementCS::from_bytes(b"\0" as &[u8]).unwrap_err().kind,
ErrorKind::ContainsNullByte
);
}
#[test]
fn from_bytes_rejects_unassigned() {
assert_eq!(
PathElementCS::from_bytes("\u{0378}".as_bytes())
.unwrap_err()
.kind,
ErrorKind::ContainsUnassignedChar
);
}
#[test]
fn from_bytes_dynamic_sensitive() {
let pe = PathElement::from_bytes(b"Hello.txt" as &[u8], CaseSensitive).unwrap();
assert_eq!(pe.normalized(), "Hello.txt");
assert_eq!(pe.case_sensitivity(), CaseSensitivity::Sensitive);
}
#[test]
fn from_bytes_overlong_null() {
let input: &[u8] = &[0x61, 0xC0, 0x80, 0x62];
assert_eq!(
PathElementCS::from_bytes(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[test]
fn from_bytes_surrogate_bytes_rejected() {
let input: &[u8] = &[0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80];
assert_eq!(
PathElementCS::from_bytes(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[test]
fn from_bytes_lone_high_surrogate_rejected() {
let input: &[u8] = &[0x61, 0xED, 0xA0, 0x80, 0x62];
assert_eq!(
PathElementCS::from_bytes(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[test]
fn from_bytes_lone_low_surrogate_rejected() {
let input: &[u8] = &[0x61, 0xED, 0xB0, 0x80, 0x62];
assert_eq!(
PathElementCS::from_bytes(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[test]
fn from_bytes_overlong_null_only() {
let input: &[u8] = &[0xC0, 0x80];
assert_eq!(
PathElementCS::from_bytes(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[test]
fn from_bytes_invalid_byte_rejected() {
let input: &[u8] = &[0x68, 0x69, 0xFF];
assert_eq!(
PathElementCS::from_bytes(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[test]
fn os_compatible_supplementary_unchanged() {
let pe = PathElementCS::new("file_😀.txt").unwrap();
assert_eq!(pe.os_compatible(), "file_😀.txt");
}
#[test]
fn os_compatible_supplementary_roundtrip() {
let pe = PathElementCS::new("file_😀.txt").unwrap();
let pe2 = PathElementCS::new(pe.os_compatible()).unwrap();
assert_eq!(pe.normalized(), pe2.normalized());
}
#[test]
fn os_compatible_multiple_supplementary() {
let pe = PathElementCS::new("𐀀_𝄞_😀").unwrap();
assert_eq!(pe.os_compatible(), "𐀀_𝄞_😀");
}
}
#[cfg(all(test, feature = "std"))]
mod os_str_tests {
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
#[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use wasm_bindgen_test::wasm_bindgen_test as test;
use crate::ErrorKind;
use crate::case_sensitivity::{CaseInsensitive, CaseSensitivity};
use crate::path_element::{PathElement, PathElementCI, PathElementCS};
#[test]
fn from_os_str_borrowed_matches_new() {
let input = OsStr::new("hello.txt");
let from_os = PathElementCS::from_os_str(input).unwrap();
let from_new = PathElementCS::new("hello.txt").unwrap();
assert_eq!(from_os.original(), from_new.original());
assert_eq!(from_os.normalized(), from_new.normalized());
assert_eq!(from_os.os_compatible(), from_new.os_compatible());
}
#[test]
fn from_os_str_owned_matches_new() {
let input = OsString::from("Hello.txt");
let from_os = PathElementCI::from_os_str(input).unwrap();
let from_new = PathElementCI::new("Hello.txt").unwrap();
assert_eq!(from_os.original(), from_new.original());
assert_eq!(from_os.normalized(), from_new.normalized());
assert_eq!(from_os.os_compatible(), from_new.os_compatible());
}
#[test]
fn from_os_str_borrowed_preserves_borrow() {
let input = OsStr::new("hello.txt");
let pe = PathElementCS::from_os_str(input).unwrap();
let orig = pe.into_original();
assert!(matches!(orig, Cow::Borrowed(_)));
}
#[test]
fn from_os_str_rejects_empty() {
assert_eq!(
PathElementCS::from_os_str(OsStr::new("")).unwrap_err().kind,
ErrorKind::Empty
);
}
#[test]
fn from_os_str_rejects_dot() {
assert_eq!(
PathElementCS::from_os_str(OsStr::new("."))
.unwrap_err()
.kind,
ErrorKind::CurrentDirectoryMarker
);
}
#[test]
fn from_os_str_rejects_dotdot() {
assert_eq!(
PathElementCS::from_os_str(OsStr::new(".."))
.unwrap_err()
.kind,
ErrorKind::ParentDirectoryMarker
);
}
#[test]
fn from_os_str_rejects_slash() {
assert_eq!(
PathElementCS::from_os_str(OsStr::new("a/b"))
.unwrap_err()
.kind,
ErrorKind::ContainsForwardSlash
);
}
#[test]
fn from_os_str_rejects_null() {
assert_eq!(
PathElementCS::from_os_str(OsStr::new("\0"))
.unwrap_err()
.kind,
ErrorKind::ContainsNullByte
);
}
#[test]
fn from_os_str_rejects_unassigned() {
assert_eq!(
PathElementCS::from_os_str(OsStr::new("\u{0378}"))
.unwrap_err()
.kind,
ErrorKind::ContainsUnassignedChar
);
}
#[cfg(unix)]
#[test]
fn from_os_str_invalid_utf8_borrowed_rejected() {
use std::os::unix::ffi::OsStrExt;
let input = OsStr::from_bytes(&[0x68, 0x69, 0xFF]);
assert_eq!(
PathElementCS::from_os_str(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[cfg(unix)]
#[test]
fn from_os_str_invalid_utf8_owned_rejected() {
use std::os::unix::ffi::OsStrExt;
let input = OsStr::from_bytes(&[0x68, 0x69, 0xFF]).to_os_string();
assert_eq!(
PathElementCS::from_os_str(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[cfg(unix)]
#[test]
fn from_os_str_surrogate_bytes_rejected() {
use std::os::unix::ffi::OsStrExt;
let input = OsStr::from_bytes(&[0x68, 0xED, 0xA0, 0x80, 0x69]);
assert_eq!(
PathElementCS::from_os_str(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[cfg(windows)]
#[test]
fn from_os_str_invalid_utf8_borrowed_rejected() {
use std::os::windows::ffi::OsStringExt;
let input = OsString::from_wide(&[0x68, 0xD800, 0x69]);
assert_eq!(
PathElementCS::from_os_str(input.as_os_str())
.unwrap_err()
.kind,
ErrorKind::InvalidUtf8
);
}
#[cfg(windows)]
#[test]
fn from_os_str_invalid_utf8_owned_rejected() {
use std::os::windows::ffi::OsStringExt;
let input = OsString::from_wide(&[0x68, 0xD800, 0x69]);
assert_eq!(
PathElementCS::from_os_str(input).unwrap_err().kind,
ErrorKind::InvalidUtf8
);
}
#[test]
fn os_str_returns_os_compatible() {
let pe = PathElementCS::new("hello.txt").unwrap();
assert_eq!(pe.os_str(), OsStr::new(pe.os_compatible()));
}
#[test]
fn into_os_str_returns_os_compatible() {
let pe = PathElementCS::new("hello.txt").unwrap();
let expected = pe.os_compatible().to_owned();
let result = pe.into_os_str();
assert_eq!(result, OsStr::new(&expected));
}
#[test]
fn into_os_str_borrows_when_no_transformation() {
let input = OsStr::new("hello.txt");
let pe = PathElementCS::from_os_str(input).unwrap();
let result = pe.into_os_str();
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, OsStr::new("hello.txt"));
}
#[test]
fn into_os_str_ci_borrows_when_already_folded() {
let input = OsStr::new("hello.txt");
let pe = PathElementCI::from_os_str(input).unwrap();
let result = pe.into_os_str();
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, OsStr::new("hello.txt"));
}
#[test]
fn into_os_str_owned_when_nfc_transforms() {
let input = OsStr::new("e\u{0301}.txt"); let pe = PathElementCS::from_os_str(input).unwrap();
let result = pe.into_os_str();
#[cfg(target_vendor = "apple")]
assert!(matches!(result, Cow::Borrowed(_)));
#[cfg(not(target_vendor = "apple"))]
assert!(matches!(result, Cow::Owned(_)));
}
#[test]
fn into_os_str_owned_when_nfd_transforms() {
let input = OsStr::new("\u{00E9}.txt"); let pe = PathElementCS::from_os_str(input).unwrap();
let result = pe.into_os_str();
#[cfg(target_vendor = "apple")]
assert!(matches!(result, Cow::Owned(_)));
#[cfg(not(target_vendor = "apple"))]
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn from_os_str_cs_matches_typed() {
let input = OsStr::new("Hello.txt");
let dyn_pe = PathElement::from_os_str_cs(input).unwrap();
let cs_pe = PathElementCS::from_os_str(input).unwrap();
assert_eq!(dyn_pe.normalized(), cs_pe.normalized());
assert_eq!(dyn_pe.os_compatible(), cs_pe.os_compatible());
assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Sensitive);
}
#[test]
fn from_os_str_ci_matches_typed() {
let input = OsStr::new("Hello.txt");
let dyn_pe = PathElement::from_os_str_ci(input).unwrap();
let ci_pe = PathElementCI::from_os_str(input).unwrap();
assert_eq!(dyn_pe.normalized(), ci_pe.normalized());
assert_eq!(dyn_pe.os_compatible(), ci_pe.os_compatible());
assert_eq!(dyn_pe.case_sensitivity(), CaseSensitivity::Insensitive);
}
#[test]
fn from_os_str_dynamic_case_sensitivity() {
let input = OsStr::new("Hello.txt");
let pe = PathElement::from_os_str(input, CaseInsensitive).unwrap();
assert_eq!(pe.normalized(), "hello.txt");
}
}