From 7802e369438810fd59ceae5352546e618a3097e1 Mon Sep 17 00:00:00 2001 From: Dmitry Shemetov Date: Fri, 19 Dec 2025 19:34:35 -0800 Subject: [PATCH 1/3] fix: touch -r: dangling symlink reference is accepted Fixes #9703 --- src/uu/touch/src/touch.rs | 18 +++++++++++++----- tests/by-util/test_touch.rs | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index f8fb3c2840e..90676d21f0d 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -359,6 +359,7 @@ pub fn uu_app() -> Command { /// Possible causes: /// - The user doesn't have permission to access the file /// - One of the directory components of the file path doesn't exist. +/// - Dangling symlink is given and -r/--reference is used. /// /// It will return an `Err` on the first error. However, for any of the files, /// if all of the following are true, it will print the error and continue touching @@ -573,14 +574,21 @@ fn update_times( } /// Get metadata of the provided path -/// If `follow` is `true`, the function will try to follow symlinks -/// If `follow` is `false` or the symlink is broken, the function will return metadata of the symlink itself +/// If `follow` is `true`, the function will try to follow symlinks. Errors if the symlink is dangling, otherwise defaults to symlink metadata. +/// If `follow` is `false`, the function will return metadata of the symlink itself fn stat(path: &Path, follow: bool) -> std::io::Result<(FileTime, FileTime)> { let metadata = if follow { - fs::metadata(path).or_else(|_| fs::symlink_metadata(path)) + match fs::metadata(path) { + // Successfully followed symlink + Ok(meta) => meta, + // Dangling symlink + Err(e) if e.kind() == ErrorKind::NotFound => return Err(e), + // Other error (?), try to get the symlink metadata + Err(_) => fs::symlink_metadata(path)?, + } } else { - fs::symlink_metadata(path) - }?; + fs::symlink_metadata(path)? + }; Ok(( FileTime::from_last_access_time(&metadata), diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 33e2682b934..b4a19da8065 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -463,6 +463,23 @@ fn test_touch_reference() { } } +#[test] +fn test_touch_reference_dangling() { + let temp_dir = tempfile::tempdir().unwrap(); + let nonexistent_target = temp_dir.path().join("nonexistent_target"); + let dangling_symlink = temp_dir.path().join("test_touch_reference_dangling"); + + std::os::unix::fs::symlink(&nonexistent_target, &dangling_symlink).unwrap(); + + new_ucmd!() + .args(&[ + "--reference", + dangling_symlink.to_str().unwrap(), + "some_file", + ]) + .fails(); +} + #[test] fn test_touch_set_date() { let (at, mut ucmd) = at_and_ucmd!(); From e540e88f85733bca714713882675b638f80b9aaf Mon Sep 17 00:00:00 2001 From: Dmitry Shemetov Date: Fri, 19 Dec 2025 20:55:46 -0800 Subject: [PATCH 2/3] test: capture message with .stderr_contains --- tests/by-util/test_touch.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index b4a19da8065..69e989fbb3d 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -477,7 +477,8 @@ fn test_touch_reference_dangling() { dangling_symlink.to_str().unwrap(), "some_file", ]) - .fails(); + .fails() + .stderr_contains("touch: failed to get attributes of"); } #[test] From 69460cef5e40c519e2806cfb05835cbd983d0787 Mon Sep 17 00:00:00 2001 From: Dmitry Shemetov Date: Fri, 19 Dec 2025 20:56:15 -0800 Subject: [PATCH 3/3] test: symlink differently on windows/not --- tests/by-util/test_touch.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 69e989fbb3d..68075867237 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -469,7 +469,14 @@ fn test_touch_reference_dangling() { let nonexistent_target = temp_dir.path().join("nonexistent_target"); let dangling_symlink = temp_dir.path().join("test_touch_reference_dangling"); - std::os::unix::fs::symlink(&nonexistent_target, &dangling_symlink).unwrap(); + #[cfg(not(windows))] + { + std::os::unix::fs::symlink(&nonexistent_target, &dangling_symlink).unwrap(); + } + #[cfg(windows)] + { + std::os::windows::fs::symlink_file(&nonexistent_target, &dangling_symlink).unwrap(); + } new_ucmd!() .args(&[