diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index b7e0f3fd965..fbbd2105713 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -7,9 +7,9 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; -use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; +use std::{fs, io}; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, set_exit_code}; @@ -372,48 +372,67 @@ impl Chmoder { for filename in files { let file = Path::new(filename); - if !file.exists() { - if file.is_symlink() { - if !self.dereference && !self.recursive { - // The file is a symlink and we should not follow it - // Don't try to change the mode of the symlink itself + + match file.try_exists() { + Ok(exists) => { + if !(exists) { + if file.is_symlink() { + if !self.dereference && !self.recursive { + // The file is a symlink and we should not follow it + // 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!(ChmodError::DanglingSymlink(filename.into())); + set_exit_code(1); + } + + if self.verbose { + println!( + "{}", + translate!("chmod-verbose-failed-dangling", "file" => filename.to_string_lossy().quote()) + ); + } + } else if !self.quiet { + show!(ChmodError::NoSuchFile(filename.into())); + } + // GNU exits with exit code 1 even if -q or --quiet are passed + // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. + set_exit_code(1); continue; - } - if self.recursive && self.traverse_symlinks == TraverseSymlinks::None { + } else if !self.dereference && file.is_symlink() { + // The file is a symlink and we should not follow it + // chmod 755 --no-dereference a/link + // should not change the permissions in this case continue; } + if self.recursive && self.preserve_root && file == Path::new("/") { + return Err(ChmodError::PreserveRoot("/".into()).into()); + } + if self.recursive { + r = self.walk_dir_with_context(file, true).and(r); + } else { + r = self.chmod_file(file).and(r); + } + } + Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { if !self.quiet { - show!(ChmodError::DanglingSymlink(filename.into())); - set_exit_code(1); + show!(ChmodError::PermissionDenied(filename.into())); } - - if self.verbose { - println!( - "{}", - translate!("chmod-verbose-failed-dangling", "file" => filename.quote()) - ); + set_exit_code(1); + } + // error must be no such file + Err(_) => { + if !self.quiet { + show!(ChmodError::NoSuchFile(filename.into())); } - } else if !self.quiet { - show!(ChmodError::NoSuchFile(filename.into())); + set_exit_code(1); } - // GNU exits with exit code 1 even if -q or --quiet are passed - // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. - set_exit_code(1); - continue; - } else if !self.dereference && file.is_symlink() { - // The file is a symlink and we should not follow it - // chmod 755 --no-dereference a/link - // should not change the permissions in this case - continue; - } - if self.recursive && self.preserve_root && file == Path::new("/") { - return Err(ChmodError::PreserveRoot("/".into()).into()); - } - if self.recursive { - r = self.walk_dir_with_context(file, true).and(r); - } else { - r = self.chmod_file(file).and(r); } } r @@ -626,6 +645,8 @@ impl Chmoder { } Ok(()) // Skip dangling symlinks } else if err.kind() == std::io::ErrorKind::PermissionDenied { + // These two filenames would normally be conditionally + // quoted, but GNU's tests expect them to always be quoted Err(ChmodError::PermissionDenied(file.into()).into()) } else { Err(ChmodError::CannotStat(file.into()).into()) diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 5e340732832..578ae639e9d 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -1389,3 +1389,23 @@ fn test_chmod_colored_output() { .stderr_contains("\x1b[31merreur\x1b[0m") // Red "erreur" in French .stderr_contains("\x1b[33m--invalid-option\x1b[0m"); // Yellow invalid option } + +#[test] +fn test_chmod_locked_dir_permission_denied() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let locked_dir = "locked"; + let file = "file"; + + at.mkdir(locked_dir); + at.touch(format!("{locked_dir}/{file}")); + at.set_mode(locked_dir, 0o000); + + scene + .ucmd() + .arg("000") + .arg(format!("{locked_dir}/{file}")) + .fails() + .stderr_contains("chmod: cannot access 'locked/file': Permission denied"); +}