Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 56 additions & 35 deletions src/uu/chmod/src/chmod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
20 changes: 20 additions & 0 deletions tests/by-util/test_chmod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Loading