Skip to content

Commit 045a75e

Browse files
authored
Merge pull request #7025 from sylvestre/chmod-L-2
chmod: add support for the deref and links options
2 parents 3eec55e + f042a9a commit 045a75e

File tree

3 files changed

+322
-44
lines changed

3 files changed

+322
-44
lines changed

src/uu/chmod/src/chmod.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use uucore::fs::display_permissions_unix;
1616
use uucore::libc::mode_t;
1717
#[cfg(not(windows))]
1818
use uucore::mode;
19+
use uucore::perms::{configure_symlink_and_recursion, TraverseSymlinks};
1920
use uucore::{format_usage, help_about, help_section, help_usage, show, show_error};
2021

2122
const ABOUT: &str = help_about!("chmod.md");
@@ -99,7 +100,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
99100
let quiet = matches.get_flag(options::QUIET);
100101
let verbose = matches.get_flag(options::VERBOSE);
101102
let preserve_root = matches.get_flag(options::PRESERVE_ROOT);
102-
let recursive = matches.get_flag(options::RECURSIVE);
103103
let fmode = match matches.get_one::<String>(options::REFERENCE) {
104104
Some(fref) => match fs::metadata(fref) {
105105
Ok(meta) => Some(meta.mode() & 0o7777),
@@ -138,6 +138,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
138138
return Err(UUsageError::new(1, "missing operand".to_string()));
139139
}
140140

141+
let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?;
142+
141143
let chmoder = Chmoder {
142144
changes,
143145
quiet,
@@ -146,6 +148,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
146148
recursive,
147149
fmode,
148150
cmode,
151+
traverse_symlinks,
152+
dereference,
149153
};
150154

151155
chmoder.chmod(&files)
@@ -237,6 +241,8 @@ struct Chmoder {
237241
recursive: bool,
238242
fmode: Option<u32>,
239243
cmode: Option<String>,
244+
traverse_symlinks: TraverseSymlinks,
245+
dereference: bool,
240246
}
241247

242248
impl Chmoder {
@@ -248,12 +254,19 @@ impl Chmoder {
248254
let file = Path::new(filename);
249255
if !file.exists() {
250256
if file.is_symlink() {
257+
if !self.dereference && !self.recursive {
258+
// The file is a symlink and we should not follow it
259+
// Don't try to change the mode of the symlink itself
260+
continue;
261+
}
251262
if !self.quiet {
252263
show!(USimpleError::new(
253264
1,
254265
format!("cannot operate on dangling symlink {}", filename.quote()),
255266
));
267+
set_exit_code(1);
256268
}
269+
257270
if self.verbose {
258271
println!(
259272
"failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)",
@@ -273,6 +286,11 @@ impl Chmoder {
273286
// So we set the exit code, because it hasn't been set yet if `self.quiet` is true.
274287
set_exit_code(1);
275288
continue;
289+
} else if !self.dereference && file.is_symlink() {
290+
// The file is a symlink and we should not follow it
291+
// chmod 755 --no-dereference a/link
292+
// should not change the permissions in this case
293+
continue;
276294
}
277295
if self.recursive && self.preserve_root && filename == "/" {
278296
return Err(USimpleError::new(
@@ -294,11 +312,23 @@ impl Chmoder {
294312

295313
fn walk_dir(&self, file_path: &Path) -> UResult<()> {
296314
let mut r = self.chmod_file(file_path);
297-
if !file_path.is_symlink() && file_path.is_dir() {
315+
// Determine whether to traverse symlinks based on `self.traverse_symlinks`
316+
let should_follow_symlink = match self.traverse_symlinks {
317+
TraverseSymlinks::All => true,
318+
TraverseSymlinks::First => {
319+
file_path == file_path.canonicalize().unwrap_or(file_path.to_path_buf())
320+
}
321+
TraverseSymlinks::None => false,
322+
};
323+
324+
// If the path is a directory (or we should follow symlinks), recurse into it
325+
if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
298326
for dir_entry in file_path.read_dir()? {
299327
let path = dir_entry?.path();
300328
if !path.is_symlink() {
301329
r = self.walk_dir(path.as_path());
330+
} else if should_follow_symlink {
331+
r = self.chmod_file(path.as_path()).and(r);
302332
}
303333
}
304334
}
@@ -314,19 +344,22 @@ impl Chmoder {
314344
}
315345
#[cfg(unix)]
316346
fn chmod_file(&self, file: &Path) -> UResult<()> {
317-
use uucore::mode::get_umask;
347+
use uucore::{mode::get_umask, perms::get_metadata};
348+
349+
let metadata = get_metadata(file, self.dereference);
318350

319-
let fperm = match fs::metadata(file) {
351+
let fperm = match metadata {
320352
Ok(meta) => meta.mode() & 0o7777,
321353
Err(err) => {
322-
if file.is_symlink() {
354+
// Handle dangling symlinks or other errors
355+
if file.is_symlink() && !self.dereference {
323356
if self.verbose {
324357
println!(
325358
"neither symbolic link {} nor referent has been changed",
326359
file.quote()
327360
);
328361
}
329-
return Ok(());
362+
return Ok(()); // Skip dangling symlinks
330363
} else if err.kind() == std::io::ErrorKind::PermissionDenied {
331364
// These two filenames would normally be conditionally
332365
// quoted, but GNU's tests expect them to always be quoted
@@ -339,6 +372,8 @@ impl Chmoder {
339372
}
340373
}
341374
};
375+
376+
// Determine the new permissions to apply
342377
match self.fmode {
343378
Some(mode) => self.change_file(fperm, mode, file)?,
344379
None => {

src/uucore/src/lib/features/perms.rs

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ fn is_root(path: &Path, would_traverse_symlink: bool) -> bool {
250250
false
251251
}
252252

253+
pub fn get_metadata(file: &Path, follow: bool) -> Result<Metadata, std::io::Error> {
254+
if follow {
255+
file.metadata()
256+
} else {
257+
file.symlink_metadata()
258+
}
259+
}
260+
253261
impl ChownExecutor {
254262
pub fn exec(&self) -> UResult<()> {
255263
let mut ret = 0;
@@ -417,11 +425,9 @@ impl ChownExecutor {
417425

418426
fn obtain_meta<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> {
419427
let path = path.as_ref();
420-
let meta = if follow {
421-
path.metadata()
422-
} else {
423-
path.symlink_metadata()
424-
};
428+
429+
let meta = get_metadata(path, follow);
430+
425431
match meta {
426432
Err(e) => {
427433
match self.verbosity.level {
@@ -516,6 +522,45 @@ pub struct GidUidOwnerFilter {
516522
}
517523
type GidUidFilterOwnerParser = fn(&ArgMatches) -> UResult<GidUidOwnerFilter>;
518524

525+
/// Determines symbolic link traversal and recursion settings based on flags.
526+
/// Returns the updated `dereference` and `traverse_symlinks` values.
527+
pub fn configure_symlink_and_recursion(
528+
matches: &ArgMatches,
529+
) -> Result<(bool, bool, TraverseSymlinks), Box<dyn crate::error::UError>> {
530+
let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) {
531+
Some(true) // Follow symlinks
532+
} else if matches.get_flag(options::dereference::NO_DEREFERENCE) {
533+
Some(false) // Do not follow symlinks
534+
} else {
535+
None // Default behavior
536+
};
537+
538+
let mut traverse_symlinks = if matches.get_flag("L") {
539+
TraverseSymlinks::All
540+
} else if matches.get_flag("H") {
541+
TraverseSymlinks::First
542+
} else {
543+
TraverseSymlinks::None
544+
};
545+
546+
let recursive = matches.get_flag(options::RECURSIVE);
547+
if recursive {
548+
if traverse_symlinks == TraverseSymlinks::None {
549+
if dereference == Some(true) {
550+
return Err(USimpleError::new(
551+
1,
552+
"-R --dereference requires -H or -L".to_string(),
553+
));
554+
}
555+
dereference = Some(false);
556+
}
557+
} else {
558+
traverse_symlinks = TraverseSymlinks::None;
559+
}
560+
561+
Ok((recursive, dereference.unwrap_or(true), traverse_symlinks))
562+
}
563+
519564
/// Base implementation for `chgrp` and `chown`.
520565
///
521566
/// An argument called `add_arg_if_not_reference` will be added to `command` if
@@ -571,34 +616,7 @@ pub fn chown_base(
571616
.unwrap_or_default();
572617

573618
let preserve_root = matches.get_flag(options::preserve_root::PRESERVE);
574-
575-
let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) {
576-
Some(true)
577-
} else if matches.get_flag(options::dereference::NO_DEREFERENCE) {
578-
Some(false)
579-
} else {
580-
None
581-
};
582-
583-
let mut traverse_symlinks = if matches.get_flag(options::traverse::TRAVERSE) {
584-
TraverseSymlinks::First
585-
} else if matches.get_flag(options::traverse::EVERY) {
586-
TraverseSymlinks::All
587-
} else {
588-
TraverseSymlinks::None
589-
};
590-
591-
let recursive = matches.get_flag(options::RECURSIVE);
592-
if recursive {
593-
if traverse_symlinks == TraverseSymlinks::None {
594-
if dereference == Some(true) {
595-
return Err(USimpleError::new(1, "-R --dereference requires -H or -L"));
596-
}
597-
dereference = Some(false);
598-
}
599-
} else {
600-
traverse_symlinks = TraverseSymlinks::None;
601-
}
619+
let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?;
602620

603621
let verbosity_level = if matches.get_flag(options::verbosity::CHANGES) {
604622
VerbosityLevel::Changes
@@ -628,7 +646,7 @@ pub fn chown_base(
628646
level: verbosity_level,
629647
},
630648
recursive,
631-
dereference: dereference.unwrap_or(true),
649+
dereference,
632650
preserve_root,
633651
files,
634652
filter,

0 commit comments

Comments
 (0)