Skip to content

Commit

Permalink
Merge pull request #7025 from sylvestre/chmod-L-2
Browse files Browse the repository at this point in the history
chmod: add support for the deref and links options
  • Loading branch information
cakebaker authored Jan 11, 2025
2 parents 3eec55e + f042a9a commit 045a75e
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 44 deletions.
47 changes: 41 additions & 6 deletions src/uu/chmod/src/chmod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use uucore::fs::display_permissions_unix;
use uucore::libc::mode_t;
#[cfg(not(windows))]
use uucore::mode;
use uucore::perms::{configure_symlink_and_recursion, TraverseSymlinks};
use uucore::{format_usage, help_about, help_section, help_usage, show, show_error};

const ABOUT: &str = help_about!("chmod.md");
Expand Down Expand Up @@ -99,7 +100,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let quiet = matches.get_flag(options::QUIET);
let verbose = matches.get_flag(options::VERBOSE);
let preserve_root = matches.get_flag(options::PRESERVE_ROOT);
let recursive = matches.get_flag(options::RECURSIVE);
let fmode = match matches.get_one::<String>(options::REFERENCE) {
Some(fref) => match fs::metadata(fref) {
Ok(meta) => Some(meta.mode() & 0o7777),
Expand Down Expand Up @@ -138,6 +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 chmoder = Chmoder {
changes,
quiet,
Expand All @@ -146,6 +148,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
recursive,
fmode,
cmode,
traverse_symlinks,
dereference,
};

chmoder.chmod(&files)
Expand Down Expand Up @@ -237,6 +241,8 @@ struct Chmoder {
recursive: bool,
fmode: Option<u32>,
cmode: Option<String>,
traverse_symlinks: TraverseSymlinks,
dereference: bool,
}

impl Chmoder {
Expand All @@ -248,12 +254,19 @@ impl Chmoder {
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
continue;
}
if !self.quiet {
show!(USimpleError::new(
1,
format!("cannot operate on dangling symlink {}", filename.quote()),
));
set_exit_code(1);
}

if self.verbose {
println!(
"failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)",
Expand All @@ -273,6 +286,11 @@ impl Chmoder {
// 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 && filename == "/" {
return Err(USimpleError::new(
Expand All @@ -294,11 +312,23 @@ impl Chmoder {

fn walk_dir(&self, file_path: &Path) -> UResult<()> {
let mut r = self.chmod_file(file_path);
if !file_path.is_symlink() && file_path.is_dir() {
// Determine whether to traverse symlinks based on `self.traverse_symlinks`
let should_follow_symlink = match self.traverse_symlinks {
TraverseSymlinks::All => true,
TraverseSymlinks::First => {
file_path == file_path.canonicalize().unwrap_or(file_path.to_path_buf())
}
TraverseSymlinks::None => false,
};

// If the path is a directory (or we should follow symlinks), recurse into it
if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
for dir_entry in file_path.read_dir()? {
let path = dir_entry?.path();
if !path.is_symlink() {
r = self.walk_dir(path.as_path());
} else if should_follow_symlink {
r = self.chmod_file(path.as_path()).and(r);
}
}
}
Expand All @@ -314,19 +344,22 @@ impl Chmoder {
}
#[cfg(unix)]
fn chmod_file(&self, file: &Path) -> UResult<()> {
use uucore::mode::get_umask;
use uucore::{mode::get_umask, perms::get_metadata};

let metadata = get_metadata(file, self.dereference);

let fperm = match fs::metadata(file) {
let fperm = match metadata {
Ok(meta) => meta.mode() & 0o7777,
Err(err) => {
if file.is_symlink() {
// Handle dangling symlinks or other errors
if file.is_symlink() && !self.dereference {
if self.verbose {
println!(
"neither symbolic link {} nor referent has been changed",
file.quote()
);
}
return Ok(());
return 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
Expand All @@ -339,6 +372,8 @@ impl Chmoder {
}
}
};

// Determine the new permissions to apply
match self.fmode {
Some(mode) => self.change_file(fperm, mode, file)?,
None => {
Expand Down
86 changes: 52 additions & 34 deletions src/uucore/src/lib/features/perms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ fn is_root(path: &Path, would_traverse_symlink: bool) -> bool {
false
}

pub fn get_metadata(file: &Path, follow: bool) -> Result<Metadata, std::io::Error> {
if follow {
file.metadata()
} else {
file.symlink_metadata()
}
}

impl ChownExecutor {
pub fn exec(&self) -> UResult<()> {
let mut ret = 0;
Expand Down Expand Up @@ -417,11 +425,9 @@ impl ChownExecutor {

fn obtain_meta<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> {
let path = path.as_ref();
let meta = if follow {
path.metadata()
} else {
path.symlink_metadata()
};

let meta = get_metadata(path, follow);

match meta {
Err(e) => {
match self.verbosity.level {
Expand Down Expand Up @@ -516,6 +522,45 @@ pub struct GidUidOwnerFilter {
}
type GidUidFilterOwnerParser = fn(&ArgMatches) -> UResult<GidUidOwnerFilter>;

/// Determines symbolic link traversal and recursion settings based on flags.
/// Returns the updated `dereference` and `traverse_symlinks` values.
pub fn configure_symlink_and_recursion(
matches: &ArgMatches,
) -> Result<(bool, bool, TraverseSymlinks), Box<dyn crate::error::UError>> {
let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) {
Some(true) // Follow symlinks
} else if matches.get_flag(options::dereference::NO_DEREFERENCE) {
Some(false) // Do not follow symlinks
} else {
None // Default behavior
};

let mut traverse_symlinks = if matches.get_flag("L") {
TraverseSymlinks::All
} else if matches.get_flag("H") {
TraverseSymlinks::First
} else {
TraverseSymlinks::None
};

let recursive = matches.get_flag(options::RECURSIVE);
if recursive {
if traverse_symlinks == TraverseSymlinks::None {
if dereference == Some(true) {
return Err(USimpleError::new(
1,
"-R --dereference requires -H or -L".to_string(),
));
}
dereference = Some(false);
}
} else {
traverse_symlinks = TraverseSymlinks::None;
}

Ok((recursive, dereference.unwrap_or(true), traverse_symlinks))
}

/// Base implementation for `chgrp` and `chown`.
///
/// An argument called `add_arg_if_not_reference` will be added to `command` if
Expand Down Expand Up @@ -571,34 +616,7 @@ pub fn chown_base(
.unwrap_or_default();

let preserve_root = matches.get_flag(options::preserve_root::PRESERVE);

let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) {
Some(true)
} else if matches.get_flag(options::dereference::NO_DEREFERENCE) {
Some(false)
} else {
None
};

let mut traverse_symlinks = if matches.get_flag(options::traverse::TRAVERSE) {
TraverseSymlinks::First
} else if matches.get_flag(options::traverse::EVERY) {
TraverseSymlinks::All
} else {
TraverseSymlinks::None
};

let recursive = matches.get_flag(options::RECURSIVE);
if recursive {
if traverse_symlinks == TraverseSymlinks::None {
if dereference == Some(true) {
return Err(USimpleError::new(1, "-R --dereference requires -H or -L"));
}
dereference = Some(false);
}
} else {
traverse_symlinks = TraverseSymlinks::None;
}
let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?;

let verbosity_level = if matches.get_flag(options::verbosity::CHANGES) {
VerbosityLevel::Changes
Expand Down Expand Up @@ -628,7 +646,7 @@ pub fn chown_base(
level: verbosity_level,
},
recursive,
dereference: dereference.unwrap_or(true),
dereference,
preserve_root,
files,
filter,
Expand Down
Loading

0 comments on commit 045a75e

Please sign in to comment.