use unicode_normalization::UnicodeNormalization;
use crate::hash::{name_hash, HashVersion, NameHash};
pub fn fold_name(name: &[u8]) -> Vec<u8> {
match std::str::from_utf8(name) {
Ok(s) => {
let nfd: String = s.nfd().collect();
caseless::default_case_fold_str(&nfd).into_bytes()
}
Err(_) => {
name.iter()
.map(|&b| {
if (0x41..=0x5A).contains(&b) {
b + 0x20
} else {
b
}
})
.collect()
}
}
}
pub fn casefold_name_hash(name: &[u8], version: HashVersion, seed: &[u32; 4]) -> NameHash {
name_hash(&fold_name(name), version, seed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fold_ascii_uppercase() {
assert_eq!(fold_name(b"HELLO"), b"hello".to_vec());
assert_eq!(fold_name(b"Hello"), b"hello".to_vec());
}
#[test]
fn fold_unicode_sharp_s() {
assert_eq!(fold_name(&[0xC3, 0x9F]), b"ss".to_vec());
}
#[test]
fn fold_unicode_latin_upper() {
let folded_upper = fold_name("Ñ".as_bytes());
let folded_lower = fold_name("ñ".as_bytes());
assert_eq!(
folded_upper, folded_lower,
"Ñ and ñ must produce the same hash input"
);
}
#[test]
fn fold_invalid_utf8_ascii_folds() {
assert_eq!(fold_name(&[0xFF, 0x41, 0x42]), vec![0xFF, 0x61, 0x62]);
}
#[test]
fn fold_empty_name() {
assert_eq!(fold_name(b""), Vec::<u8>::new());
}
const DEBUGFS_SEED: [u32; 4] = [0x6745_2301, 0xefcd_ab89, 0x6745_2301, 0xefcd_ab89];
const DEBUGFS_CASEFOLD: &[(u8, &str, u32, u32)] = &[
(0, "HELLO", 0x32252546, 0x00000000),
(0, "Straße", 0xad2ed8da, 0x00000000),
(0, "Café", 0xb57af1a2, 0x00000000),
(0, "İstanbul", 0x7993d1e8, 0x00000000),
(0, "file", 0x10c30fca, 0x00000000),
(0, "Dž", 0xf3cbc72e, 0x00000000),
(1, "HELLO", 0xa26e4a80, 0x97e5b7f7),
(1, "Straße", 0xfd935386, 0x310eec3f),
(1, "Café", 0xb4f033e6, 0xc53b7dc9),
(1, "İstanbul", 0xea3181a8, 0x95cdbb2d),
(1, "file", 0x2fa08550, 0xa2347c70),
(1, "Dž", 0xecb9513e, 0x53db85db),
(2, "HELLO", 0x6f5bb1a8, 0x231917c2),
(2, "Straße", 0xd6e5379a, 0x7d22f805),
(2, "Café", 0x65e6583e, 0x6e22bfb6),
(2, "İstanbul", 0xea09454c, 0x000466d6),
(2, "file", 0x53fcf74e, 0x15e4b547),
(2, "Dž", 0x60e7aab0, 0xddc780d6),
(3, "HELLO", 0x32252546, 0x00000000),
(3, "Straße", 0xad2ed8da, 0x00000000),
(3, "Café", 0x710181a6, 0x00000000),
(3, "İstanbul", 0x32e89dbc, 0x00000000),
(3, "file", 0x10c30fca, 0x00000000),
(3, "Dž", 0xb34beb2c, 0x00000000),
(4, "HELLO", 0xa26e4a80, 0x97e5b7f7),
(4, "Straße", 0xfd935386, 0x310eec3f),
(4, "Café", 0xb5763ba0, 0x9b8175dc),
(4, "İstanbul", 0x1c5d5e6e, 0x36372989),
(4, "file", 0x2fa08550, 0xa2347c70),
(4, "Dž", 0xd79841e0, 0xc3767231),
(5, "HELLO", 0x6f5bb1a8, 0x231917c2),
(5, "Straße", 0xd6e5379a, 0x7d22f805),
(5, "Café", 0xc1ea8e5e, 0xf3ae206f),
(5, "İstanbul", 0x6be30f56, 0x0d8d60e7),
(5, "file", 0x53fcf74e, 0x15e4b547),
(5, "Dž", 0x1d244eee, 0xc7690204),
];
#[test]
fn casefold_hash_matches_debugfs_for_every_hash_version() {
for &(version, name, major, minor) in DEBUGFS_CASEFOLD {
let v = HashVersion::from_u8(version).unwrap();
let h = casefold_name_hash(name.as_bytes(), v, &DEBUGFS_SEED);
assert_eq!(
(h.major, h.minor),
(major, minor),
"v{version} {name:?}: got ({:#010x}, {:#010x})",
h.major,
h.minor
);
}
}
#[test]
fn casefold_hash_is_case_insensitive() {
let seed = [1u32, 2, 3, 4];
for version in 0..=5 {
let v = HashVersion::from_u8(version).unwrap();
let a = casefold_name_hash(b"README", v, &seed);
let b = casefold_name_hash(b"readme", v, &seed);
let c = casefold_name_hash(b"ReadMe", v, &seed);
assert_eq!(a, b, "v{version}");
assert_eq!(a, c, "v{version}");
}
}
#[test]
fn casefold_hash_differs_on_different_names() {
let seed = [1u32, 2, 3, 4];
let a = casefold_name_hash(b"hello", HashVersion::HalfMd4, &seed);
let b = casefold_name_hash(b"hellw", HashVersion::HalfMd4, &seed);
assert_ne!(a.major, b.major);
}
#[test]
fn casefold_low_bit_is_zero() {
let seed = [0xdeadbeefu32, 0, 0, 0];
for version in 0..=5 {
let v = HashVersion::from_u8(version).unwrap();
for name in [b"foo".as_slice(), b"BAR", b"mixedCase"] {
let h = casefold_name_hash(name, v, &seed);
assert_eq!(h.major & 1, 0);
}
}
}
}