diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index c45950a54..9b4ddbebd 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -138,7 +138,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(UUsageError::new(1, "missing operand".to_string())); } - let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?; + let (recursive, dereference, traverse_symlinks) = + configure_symlink_and_recursion(&matches, TraverseSymlinks::First)?; let chmoder = Chmoder { changes, @@ -259,6 +260,10 @@ impl Chmoder { // Don't try to change the mode of the symlink itself continue; } + if self.recursive && self.traverse_symlinks == TraverseSymlinks::None { + continue; + } + if !self.quiet { show!(USimpleError::new( 1, diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 4addccf24..653da7303 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -507,6 +507,7 @@ type GidUidFilterOwnerParser = fn(&ArgMatches) -> UResult; /// Returns the updated `dereference` and `traverse_symlinks` values. pub fn configure_symlink_and_recursion( matches: &ArgMatches, + default_traverse_symlinks: TraverseSymlinks, ) -> Result<(bool, bool, TraverseSymlinks), Box> { let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) { Some(true) // Follow symlinks @@ -516,12 +517,13 @@ pub fn configure_symlink_and_recursion( None // Default behavior }; - let mut traverse_symlinks = if matches.get_flag("L") { - TraverseSymlinks::All + let mut traverse_symlinks = default_traverse_symlinks; + if matches.get_flag("L") { + traverse_symlinks = TraverseSymlinks::All } else if matches.get_flag("H") { - TraverseSymlinks::First - } else { - TraverseSymlinks::None + traverse_symlinks = TraverseSymlinks::First + } else if matches.get_flag("P") { + traverse_symlinks = TraverseSymlinks::None }; let recursive = matches.get_flag(options::RECURSIVE); @@ -597,7 +599,8 @@ pub fn chown_base( .unwrap_or_default(); let preserve_root = matches.get_flag(options::preserve_root::PRESERVE); - let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?; + let (recursive, dereference, traverse_symlinks) = + configure_symlink_and_recursion(&matches, TraverseSymlinks::None)?; let verbosity_level = if matches.get_flag(options::verbosity::CHANGES) { VerbosityLevel::Changes diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 310bdb9d2..c31c64c65 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -878,7 +878,7 @@ fn test_chmod_symlink_target_no_dereference() { } #[test] -fn test_chmod_symlink_to_dangling_recursive() { +fn test_chmod_symlink_recursive_final_traversal_flag() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -891,9 +891,14 @@ fn test_chmod_symlink_to_dangling_recursive() { .ucmd() .arg("755") .arg("-R") + .arg("-H") + .arg("-L") + .arg("-H") + .arg("-L") + .arg("-P") .arg(symlink) - .fails() - .stderr_is("chmod: cannot operate on dangling symlink 'symlink'\n"); + .succeeds() + .no_output(); assert_eq!( at.symlink_metadata(symlink).permissions().mode(), get_expected_symlink_permissions(), @@ -903,9 +908,73 @@ fn test_chmod_symlink_to_dangling_recursive() { ); } +#[test] +fn test_chmod_symlink_to_dangling_recursive_no_traverse() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dangling_target = "nonexistent_file"; + let symlink = "symlink"; + + at.symlink_file(dangling_target, symlink); + + scene + .ucmd() + .arg("755") + .arg("-R") + .arg("-P") + .arg(symlink) + .succeeds() + .no_output(); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions(), + "Expected symlink permissions: {:o}, but got: {:o}", + get_expected_symlink_permissions(), + at.symlink_metadata(symlink).permissions().mode() + ); +} + +#[test] +fn test_chmod_dangling_symlink_recursive_combos() { + let error_scenarios = [vec!["-R"], vec!["-R", "-H"], vec!["-R", "-L"]]; + + for flags in error_scenarios { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dangling_target = "nonexistent_file"; + let symlink = "symlink"; + + at.symlink_file(dangling_target, symlink); + + let mut ucmd = scene.ucmd(); + for f in &flags { + ucmd.arg(f); + } + ucmd.arg("u+x") + .umask(0o022) + .arg(symlink) + .fails() + .stderr_is("chmod: cannot operate on dangling symlink 'symlink'\n"); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions(), + "Expected symlink permissions: {:o}, but got: {:o}", + get_expected_symlink_permissions(), + at.symlink_metadata(symlink).permissions().mode() + ); + } +} + #[test] fn test_chmod_traverse_symlink_combo() { let scenarios = [ + ( + vec!["-R"], // Should default to "-H" + 0o100_664, + get_expected_symlink_permissions(), + ), ( vec!["-R", "-H"], 0o100_664,