From 88bc1c5fc3eac1947cb027559e4a7917248325a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=E1=BA=A3=20th=E1=BA=BF=20gi=E1=BB=9Bi=20l=C3=A0=20Rust?= <90588855+naoNao89@users.noreply.github.com> Date: Thu, 16 Oct 2025 07:19:14 +0700 Subject: [PATCH] Merge pull request #8911 from naoNao89/fix-dirname-trailing-dot Fix dirname handling of paths ending in `/.` --- src/uu/dirname/src/dirname.rs | 78 +++++++++++++++++++---- tests/by-util/test_dirname.rs | 114 ++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 13 deletions(-) diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index fe952bb7b..3399b4a03 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -18,6 +18,53 @@ mod options { pub const DIR: &str = "dir"; } +/// Handle the special case where a path ends with "/." +/// +/// This matches GNU/POSIX behavior where `dirname("/home/dos/.")` returns "/home/dos" +/// rather than "/home" (which would be the result of `Path::parent()` due to normalization). +/// Per POSIX.1-2017 dirname specification and GNU coreutils manual: +/// - POSIX: +/// - GNU: +/// +/// dirname should do simple string manipulation without path normalization. +/// See issue #8910 and similar fix in basename (#8373, commit c5268a897). +/// +/// Returns `Some(())` if the special case was handled (output already printed), +/// or `None` if normal `Path::parent()` logic should be used. +fn handle_trailing_dot(path_bytes: &[u8]) -> Option<()> { + if !path_bytes.ends_with(b"/.") { + return None; + } + + // Strip the "/." suffix and print the result + if path_bytes.len() == 2 { + // Special case: "/." -> "/" + print!("/"); + Some(()) + } else { + // General case: "/home/dos/." -> "/home/dos" + let stripped = &path_bytes[..path_bytes.len() - 2]; + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let result = std::ffi::OsStr::from_bytes(stripped); + print_verbatim(result).unwrap(); + Some(()) + } + #[cfg(not(unix))] + { + // On non-Unix, fall back to lossy conversion + if let Ok(s) = std::str::from_utf8(stripped) { + print!("{s}"); + Some(()) + } else { + // Can't handle non-UTF-8 on non-Unix, fall through to normal logic + None + } + } + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; @@ -35,20 +82,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } for path in &dirnames { - let p = Path::new(path); - match p.parent() { - Some(d) => { - if d.components().next().is_none() { - print!("."); - } else { - print_verbatim(d).unwrap(); + let path_bytes = uucore::os_str_as_bytes(path.as_os_str()).unwrap_or(&[]); + + if handle_trailing_dot(path_bytes).is_none() { + // Normal path handling using Path::parent() + let p = Path::new(path); + match p.parent() { + Some(d) => { + if d.components().next().is_none() { + print!("."); + } else { + print_verbatim(d).unwrap(); + } } - } - None => { - if p.is_absolute() || path.as_os_str() == "/" { - print!("/"); - } else { - print!("."); + None => { + if p.is_absolute() || path.as_os_str() == "/" { + print!("/"); + } else { + print!("."); + } } } } diff --git a/tests/by-util/test_dirname.rs b/tests/by-util/test_dirname.rs index d5aedc45a..c7cdf3a46 100644 --- a/tests/by-util/test_dirname.rs +++ b/tests/by-util/test_dirname.rs @@ -102,3 +102,117 @@ fn test_emoji_handling() { .succeeds() .stdout_is("/🌟/emoji/path\u{0}"); } + +#[test] +fn test_trailing_dot() { + // Basic case: path ending with /. should return parent without stripping last component + // This matches GNU coreutils behavior and fixes issue #8910 + new_ucmd!() + .arg("/home/dos/.") + .succeeds() + .stdout_is("/home/dos\n"); + + // Root with dot + new_ucmd!().arg("/.").succeeds().stdout_is("/\n"); + + // Relative path with /. + new_ucmd!().arg("hello/.").succeeds().stdout_is("hello\n"); + + // Deeper path with /. + new_ucmd!() + .arg("/foo/bar/baz/.") + .succeeds() + .stdout_is("/foo/bar/baz\n"); +} + +#[test] +fn test_trailing_dot_with_zero_flag() { + // Test that -z flag works correctly with /. paths + new_ucmd!() + .arg("-z") + .arg("/home/dos/.") + .succeeds() + .stdout_is("/home/dos\u{0}"); + + new_ucmd!() + .arg("--zero") + .arg("/.") + .succeeds() + .stdout_is("/\u{0}"); +} + +#[test] +fn test_trailing_dot_multiple_paths() { + // Test multiple paths, some with /. suffix + new_ucmd!() + .args(&["/home/dos/.", "/var/log", "/tmp/."]) + .succeeds() + .stdout_is("/home/dos\n/var\n/tmp\n"); +} + +#[test] +fn test_trailing_dot_edge_cases() { + // Double slash before dot (should still work) + new_ucmd!() + .arg("/home/dos//.") + .succeeds() + .stdout_is("/home/dos/\n"); + + // Path with . in middle (should use normal logic) + new_ucmd!() + .arg("/path/./to/file") + .succeeds() + .stdout_is("/path/./to\n"); +} + +#[test] +fn test_trailing_dot_emoji() { + // Emoji paths with /. suffix + new_ucmd!() + .arg("/🌍/path/.") + .succeeds() + .stdout_is("/🌍/path\n"); + + new_ucmd!().arg("/🎉/🚀/.").succeeds().stdout_is("/🎉/🚀\n"); +} + +#[test] +#[cfg(unix)] +fn test_trailing_dot_non_utf8() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + // Create a path with non-UTF-8 bytes ending in /. + let non_utf8_bytes = b"/test_\xFF\xFE/."; + let non_utf8_path = OsStr::from_bytes(non_utf8_bytes); + + // Test that dirname handles non-UTF-8 paths with /. suffix + let result = new_ucmd!().arg(non_utf8_path).succeeds(); + + // The output should be the path without the /. suffix + let output = result.stdout_str_lossy(); + assert!(!output.is_empty()); + assert!(output.contains("test_")); + // Should not contain the . at the end + assert!(!output.trim().ends_with('.')); +} + +#[test] +fn test_existing_behavior_preserved() { + // Ensure we didn't break existing test cases + // These tests verify backward compatibility + + // Normal paths without /. should work as before + new_ucmd!().arg("/home/dos").succeeds().stdout_is("/home\n"); + + new_ucmd!() + .arg("/home/dos/") + .succeeds() + .stdout_is("/home\n"); + + // Parent directory references + new_ucmd!() + .arg("/home/dos/..") + .succeeds() + .stdout_is("/home/dos\n"); +}