diff --git a/src/utils.rs b/src/utils.rs index 29ab93d..d73c2ef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -163,11 +163,13 @@ pub fn quote_shell_argument(arg: &[u8]) -> Cow<'_, [u8]> { Cow::Owned(quoted) } -/// Returns `name` in lowercase, to match file names ignoring case. +/// Returns `name` in lowercase, to match file names ignoring case. On Unix, +/// the POSIX locale only has the ASCII letters. pub fn fold_file_name_case(name: &OsStr) -> Vec { + let ascii_only = cfg!(unix) && is_posix_locale("LC_CTYPE"); match name.to_str() { - Some(name) => name.to_lowercase().into_bytes(), - None => name.as_encoded_bytes().to_ascii_lowercase(), + Some(name) if !ascii_only => name.to_lowercase().into_bytes(), + _ => name.as_encoded_bytes().to_ascii_lowercase(), } } diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index c6d4140..9585124 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -574,6 +574,57 @@ fn recursive_loop_on_one_side_only() { .stderr_is("diff: one/sub/loop/sub/loop: recursive directory loop\n"); } +// The POSIX locale only knows the ASCII letters, so --ignore-file-name-case +// does not make É and é the same name there. +#[cfg(unix)] +#[test] +fn ignore_file_name_case_is_ascii_only_in_posix_locale() { + let (at, ts) = at_and_ts!(); + for (side, accented, plain) in [("one", "É", "Plain"), ("two", "é", "plain")] { + at.mkdir(side); + at.write(&format!("{side}/{accented}"), "a\n"); + at.write(&format!("{side}/{plain}"), "a\n"); + } + if at.file_exists("two/É") { + // The file system is not case-sensitive, so it finds é under + // either name whatever the locale. + return; + } + // LC_ALL overrides LC_CTYPE, which overrides LANG; an empty value counts + // as unset, which replaces the LC_ALL=C that ucmd sets. + let diff = |locale: &[(&str, &str)], args: &[&str]| { + ts.ucmd() + .env("LC_ALL", "") + .envs(locale.iter().copied()) + .arg("--ignore-file-name-case") + .args(args) + .run() + }; + + for locale in [ + &[("LC_ALL", "C")][..], + &[("LC_ALL", "POSIX"), ("LC_CTYPE", "en_US.UTF-8")], + &[("LC_CTYPE", "C"), ("LANG", "en_US.UTF-8")], + &[("LANG", "C")], + &[], + ] { + diff(locale, &["-r", "one", "two"]) + .code_is(1) + .stdout_only("Only in one: É\nOnly in two: é\n"); + diff(locale, &["one/É", "two"]) + .code_is(2) + .stderr_contains("No such file or directory"); + } + for locale in [ + &[("LC_ALL", "C.UTF-8")][..], + &[("LC_CTYPE", "en_US.UTF-8"), ("LANG", "C")], + &[("LANG", "fr_FR.UTF-8")], + ] { + diff(locale, &["-r", "one", "two"]).code_is(0).no_output(); + diff(locale, &["one/É", "two"]).code_is(0); + } +} + #[test] fn directory_name_matching() { let (at, ts) = at_and_ts!();