use std::cmp::Ordering;
use std::collections::HashMap;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::OnceLock;
pub struct CaseMapper(HashMap<char, char>);
impl CaseMapper {
fn new() -> CaseMapper {
CaseMapper(include!("uppercase.txt").iter().copied().collect())
}
fn simple_uppercase(&self, c: char) -> char {
self.0.get(&c).copied().or(c.to_uppercase().next()).unwrap_or_default()
}
}
const MAX_NAME_LEN: usize = 31;
fn cfb_uppercase_char(c: char) -> char {
static CASE_MAPPER: OnceLock<CaseMapper> = OnceLock::new();
let case_mapper = CASE_MAPPER.get_or_init(CaseMapper::new);
case_mapper.simple_uppercase(c)
}
pub fn compare_names(name1: &str, name2: &str) -> Ordering {
if name1.is_ascii() && name2.is_ascii() {
match name1.len().cmp(&name2.len()) {
Ordering::Equal => {
for (left, right) in name1.bytes().zip(name2.bytes()) {
let left = left.to_ascii_uppercase();
let right = right.to_ascii_uppercase();
match left.cmp(&right) {
Ordering::Equal => {}
other => return other,
}
}
Ordering::Equal
}
other => other,
}
} else {
match name1.encode_utf16().count().cmp(&name2.encode_utf16().count()) {
Ordering::Equal => {
let n1 = name1.chars().map(cfb_uppercase_char);
let n2 = name2.chars().map(cfb_uppercase_char);
n1.cmp(n2)
}
other => other,
}
}
}
pub fn validate_name(name: &str) -> io::Result<Vec<u16>> {
let name_utf16: Vec<u16> =
name.encode_utf16().take(MAX_NAME_LEN + 1).collect();
if name_utf16.len() > MAX_NAME_LEN {
invalid_input!(
"Object name cannot be more than {} UTF-16 code units (was {})",
MAX_NAME_LEN,
name.encode_utf16().count()
);
}
for &chr in &['/', '\\', ':', '!'] {
if name.contains(chr) {
invalid_input!("Object name cannot contain {} character", chr);
}
}
Ok(name_utf16)
}
pub fn name_chain_from_path(path: &Path) -> io::Result<Vec<&str>> {
let mut names: Vec<&str> = Vec::new();
for component in path.components() {
match component {
Component::Prefix(_) => {
invalid_input!("Invalid path (must not have prefix)");
}
Component::RootDir => names.clear(),
Component::CurDir => {}
Component::ParentDir => {
if names.pop().is_none() {
invalid_input!("Invalid path (must be within root)");
}
}
Component::Normal(osstr) => match osstr.to_str() {
Some(name) => names.push(name),
None => invalid_input!("Non UTF-8 path"),
},
}
}
Ok(names)
}
pub fn path_from_name_chain(names: &[&str]) -> PathBuf {
let mut path = PathBuf::from("/");
for name in names {
path.push(name);
}
path
}
#[cfg(test)]
mod tests {
use super::{
cfb_uppercase_char, compare_names, name_chain_from_path,
path_from_name_chain, validate_name,
};
use std::cmp::Ordering;
use std::path::{Path, PathBuf};
#[test]
fn name_ordering() {
assert_eq!(compare_names("foobar", "FOOBAR"), Ordering::Equal);
assert_eq!(compare_names("foo", "barfoo"), Ordering::Less);
assert_eq!(compare_names("Foo", "bar"), Ordering::Greater);
assert_eq!(
compare_names(
"ÖÇÔÍÒÄÁØÐÔÞ3ׯXVÔÄHMDQ==",
"ßYÜ0MÈÝEÄÄÂKÏÓÉDÀP5ÃÝA=="
),
Ordering::Less
);
assert_eq!(
compare_names(
"É1EDAÉNÅPUOÈÒKÔÓCÓÇÇPÐ==",
"ßÕFÆRDÜÐNÔCÄ2PKQÃFAFMA=="
),
Ordering::Less
);
let uppercase = "ßQÑ52Ç4ÅÁÔÂFÛCWCÙÂNË5Q=="
.chars()
.map(cfb_uppercase_char)
.collect::<String>();
assert_eq!("ßQÑ52Ç4ÅÁÔÂFÛCWCÙÂNË5Q==", uppercase);
assert_eq!(
compare_names(
"ÜL43ÁMÆÛÏEKZÅYWÚÓVDÙÄÀ==",
"ßQÑ52Ç4ÅÁÔÂFÛCWCÙÂNË5Q=="
),
Ordering::Less
);
}
#[test]
fn short_name_is_valid() {
assert_eq!(
validate_name("Foobar").unwrap(),
vec![70, 111, 111, 98, 97, 114]
);
}
#[test]
#[should_panic(
expected = "Object name cannot be more than 31 UTF-16 code units \
(was 35)"
)]
fn long_name_is_invalid() {
validate_name("ThisNameIsMostDefinitelyMuchTooLong").unwrap();
}
#[test]
#[should_panic(expected = "Object name cannot contain / character")]
fn name_with_slash_is_invalid() {
validate_name("foo/bar").unwrap();
}
#[test]
fn absolute_path_is_valid() {
assert_eq!(
name_chain_from_path(Path::new("/foo/bar/baz/")).unwrap(),
vec!["foo", "bar", "baz"]
);
}
#[test]
fn relative_path_is_valid() {
assert_eq!(
name_chain_from_path(Path::new("foo/bar/baz")).unwrap(),
vec!["foo", "bar", "baz"]
);
}
#[test]
fn path_with_parents_is_valid() {
assert_eq!(
name_chain_from_path(Path::new("foo/bar/../baz")).unwrap(),
vec!["foo", "baz"]
);
}
#[test]
#[should_panic(expected = "Invalid path (must be within root)")]
fn parent_of_root_is_invalid() {
name_chain_from_path(Path::new("foo/../../baz")).unwrap();
}
#[test]
fn canonical_path_is_absolute() {
let path = Path::new("foo/bar/../baz");
let names = name_chain_from_path(path).unwrap();
assert_eq!(path_from_name_chain(&names), PathBuf::from("/foo/baz"));
}
#[ignore = "add icu_casemap to dependencies to regenerate exceptional uppercase chars"]
#[test]
fn uppercase_generation() {
use std::fmt;
struct AsArray(Vec<(char, char)>);
impl fmt::Display for AsArray {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[")?;
let s = self
.0
.iter()
.map(|(input, output)| format!("('{input}', '{output}')"))
.collect::<Vec<_>>()
.join(", ");
f.write_str(&s)?;
f.write_str("]")
}
}
let case_mapper = super::CaseMapper::new();
let mut nonequal = Vec::new();
for i in 0..u32::MAX {
let Some(c) = char::from_u32(i) else {
continue;
};
let u1 = case_mapper.simple_uppercase(c);
let mut uppers = c.to_uppercase();
let u2 = uppers.next().unwrap();
if u1 != u2 || uppers.next().is_some() {
nonequal.push((c, u1));
}
}
let array = AsArray(nonequal);
std::fs::write("src/internal/uppercase.txt", array.to_string())
.unwrap();
}
}