1use clap::{Arg, ArgAction, Command};
9use std::ffi::OsString;
10use std::fs;
11use std::os::unix::fs::{MetadataExt, PermissionsExt};
12use std::path::{Path, PathBuf};
13use thiserror::Error;
14use uucore::display::Quotable;
15use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, set_exit_code};
16use uucore::fs::display_permissions_unix;
17use uucore::libc::mode_t;
18use uucore::mode;
19use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion};
20
21#[cfg(all(unix, not(target_os = "redox")))]
22use uucore::safe_traversal::{DirFd, SymlinkBehavior};
23use uucore::{format_usage, show, show_error};
24
25use uucore::translate;
26
27#[derive(Debug, Error)]
28enum ChmodError {
29 #[error("{}", translate!("chmod-error-cannot-stat", "file" => _0.quote()))]
30 CannotStat(PathBuf),
31 #[error("{}", translate!("chmod-error-dangling-symlink", "file" => _0.quote()))]
32 DanglingSymlink(PathBuf),
33 #[error("{}", translate!("chmod-error-no-such-file", "file" => _0.quote()))]
34 NoSuchFile(PathBuf),
35 #[error("{}", translate!("chmod-error-preserve-root", "file" => _0.quote()))]
36 PreserveRoot(PathBuf),
37 #[error("{}", translate!("chmod-error-permission-denied", "file" => _0.quote()))]
38 PermissionDenied(PathBuf),
39 #[error("{}", translate!("chmod-error-new-permissions", "file" => _0.maybe_quote(), "actual" => _1.clone(), "expected" => _2.clone()))]
40 NewPermissions(PathBuf, String, String),
41}
42
43impl UError for ChmodError {}
44
45mod options {
46 pub const HELP: &str = "help";
47 pub const CHANGES: &str = "changes";
48 pub const QUIET: &str = "quiet"; pub const VERBOSE: &str = "verbose";
50 pub const NO_PRESERVE_ROOT: &str = "no-preserve-root";
51 pub const PRESERVE_ROOT: &str = "preserve-root";
52 pub const REFERENCE: &str = "RFILE";
53 pub const RECURSIVE: &str = "recursive";
54 pub const MODE: &str = "MODE";
55 pub const FILE: &str = "FILE";
56}
57
58fn extract_negative_modes(mut args: impl uucore::Args) -> (Option<String>, Vec<OsString>) {
69 let (parsed_cmode_vec, pre_double_hyphen_args): (Vec<OsString>, Vec<OsString>) =
72 args.by_ref().take_while(|a| a != "--").partition(|arg| {
73 let arg = if let Some(arg) = arg.to_str() {
74 arg.to_string()
75 } else {
76 return false;
77 };
78 arg.len() >= 2
79 && arg.starts_with('-')
80 && matches!(
81 arg.chars().nth(1).unwrap(),
82 'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7'
83 )
84 });
85
86 let mut clean_args = Vec::new();
87 if !parsed_cmode_vec.is_empty() {
88 clean_args.push("w".into());
91 }
92 clean_args.extend(pre_double_hyphen_args);
93
94 if let Some(arg) = args.next() {
95 clean_args.push("--".into());
98 clean_args.push(arg);
99 }
100 clean_args.extend(args);
101
102 let parsed_cmode = Some(
103 parsed_cmode_vec
104 .iter()
105 .map(|s| s.to_str().unwrap())
106 .collect::<Vec<&str>>()
107 .join(","),
108 )
109 .filter(|s| !s.is_empty());
110 (parsed_cmode, clean_args)
111}
112
113#[uucore::main]
114pub fn uumain(args: impl uucore::Args) -> UResult<()> {
115 let (parsed_cmode, args) = extract_negative_modes(args.skip(1)); let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
117
118 let changes = matches.get_flag(options::CHANGES);
119 let quiet = matches.get_flag(options::QUIET);
120 let verbose = matches.get_flag(options::VERBOSE);
121 let preserve_root = matches.get_flag(options::PRESERVE_ROOT);
122 let fmode = match matches.get_one::<OsString>(options::REFERENCE) {
123 Some(fref) => match fs::metadata(fref) {
124 Ok(meta) => Some(meta.mode() & 0o7777),
125 Err(_) => {
126 return Err(ChmodError::CannotStat(fref.into()).into());
127 }
128 },
129 None => None,
130 };
131
132 let modes = matches.get_one::<String>(options::MODE);
133 let cmode = if let Some(parsed_cmode) = parsed_cmode {
134 parsed_cmode
135 } else {
136 modes.unwrap().to_owned() };
138 let mut files: Vec<OsString> = matches
139 .get_many::<OsString>(options::FILE)
140 .map(|v| v.cloned().collect())
141 .unwrap_or_default();
142 let cmode = if fmode.is_some() {
143 files.push(OsString::from(cmode));
147 None
148 } else {
149 Some(cmode)
150 };
151
152 if files.is_empty() {
153 return Err(UUsageError::new(
154 1,
155 translate!("chmod-error-missing-operand"),
156 ));
157 }
158
159 let (recursive, dereference, traverse_symlinks) =
160 configure_symlink_and_recursion(&matches, TraverseSymlinks::First)?;
161
162 let chmoder = Chmoder {
163 changes,
164 quiet,
165 verbose,
166 preserve_root,
167 recursive,
168 fmode,
169 cmode,
170 traverse_symlinks,
171 dereference,
172 };
173
174 chmoder.chmod(&files)
175}
176
177pub fn uu_app() -> Command {
178 Command::new(uucore::util_name())
179 .version(uucore::crate_version!())
180 .about(translate!("chmod-about"))
181 .override_usage(format_usage(&translate!("chmod-usage")))
182 .help_template(uucore::localized_help_template(uucore::util_name()))
183 .args_override_self(true)
184 .infer_long_args(true)
185 .no_binary_name(true)
186 .disable_help_flag(true)
187 .after_help(translate!("chmod-after-help"))
188 .arg(
189 Arg::new(options::HELP)
190 .long(options::HELP)
191 .help(translate!("chmod-help-print-help"))
192 .action(ArgAction::Help),
193 )
194 .arg(
195 Arg::new(options::CHANGES)
196 .long(options::CHANGES)
197 .short('c')
198 .help(translate!("chmod-help-changes"))
199 .action(ArgAction::SetTrue),
200 )
201 .arg(
202 Arg::new(options::QUIET)
203 .long(options::QUIET)
204 .visible_alias("silent")
205 .short('f')
206 .help(translate!("chmod-help-quiet"))
207 .action(ArgAction::SetTrue),
208 )
209 .arg(
210 Arg::new(options::VERBOSE)
211 .long(options::VERBOSE)
212 .short('v')
213 .help(translate!("chmod-help-verbose"))
214 .action(ArgAction::SetTrue),
215 )
216 .arg(
217 Arg::new(options::NO_PRESERVE_ROOT)
218 .long(options::NO_PRESERVE_ROOT)
219 .help(translate!("chmod-help-no-preserve-root"))
220 .action(ArgAction::SetTrue),
221 )
222 .arg(
223 Arg::new(options::PRESERVE_ROOT)
224 .long(options::PRESERVE_ROOT)
225 .help(translate!("chmod-help-preserve-root"))
226 .action(ArgAction::SetTrue),
227 )
228 .arg(
229 Arg::new(options::RECURSIVE)
230 .long(options::RECURSIVE)
231 .short('R')
232 .help(translate!("chmod-help-recursive"))
233 .action(ArgAction::SetTrue),
234 )
235 .arg(
236 Arg::new(options::REFERENCE)
237 .long("reference")
238 .value_hint(clap::ValueHint::FilePath)
239 .value_parser(clap::value_parser!(OsString))
240 .help(translate!("chmod-help-reference")),
241 )
242 .arg(
243 Arg::new(options::MODE).required_unless_present(options::REFERENCE),
244 )
248 .arg(
249 Arg::new(options::FILE)
250 .required_unless_present(options::MODE)
251 .action(ArgAction::Append)
252 .value_hint(clap::ValueHint::AnyPath)
253 .value_parser(clap::value_parser!(OsString)),
254 )
255 .args(uucore::perms::common_args())
257}
258
259struct Chmoder {
260 changes: bool,
261 quiet: bool,
262 verbose: bool,
263 preserve_root: bool,
264 recursive: bool,
265 fmode: Option<u32>,
266 cmode: Option<String>,
267 traverse_symlinks: TraverseSymlinks,
268 dereference: bool,
269}
270
271impl Chmoder {
272 fn calculate_new_mode(&self, current_mode: u32, is_dir: bool) -> UResult<(u32, u32)> {
275 if let Some(mode) = self.fmode {
276 Ok((mode, mode))
277 } else {
278 let cmode_unwrapped = self.cmode.clone().unwrap();
279 let mut new_mode = current_mode;
280 let mut naively_expected_new_mode = current_mode;
281
282 for mode in cmode_unwrapped.split(',') {
283 let result = if mode.chars().any(|c| c.is_ascii_digit()) {
284 mode::parse_numeric(new_mode, mode, is_dir).map(|v| (v, v))
285 } else {
286 mode::parse_symbolic(new_mode, mode, mode::get_umask(), is_dir).map(|m| {
287 let naive_mode =
289 mode::parse_symbolic(naively_expected_new_mode, mode, 0, is_dir)
290 .unwrap(); (m, naive_mode)
292 })
293 };
294
295 match result {
296 Ok((mode, naive_mode)) => {
297 new_mode = mode;
298 naively_expected_new_mode = naive_mode;
299 }
300 Err(f) => {
301 return if self.quiet {
302 Err(ExitCode::new(1))
303 } else {
304 Err(USimpleError::new(1, f))
305 };
306 }
307 }
308 }
309 Ok((new_mode, naively_expected_new_mode))
310 }
311 }
312
313 fn report_permission_change(&self, file_path: &Path, old_mode: u32, new_mode: u32) {
315 if self.verbose || self.changes {
316 let current_permissions = display_permissions_unix(old_mode as mode_t, false);
317 let new_permissions = display_permissions_unix(new_mode as mode_t, false);
318
319 if new_mode != old_mode {
320 println!(
321 "mode of {} changed from {old_mode:04o} ({current_permissions}) to {new_mode:04o} ({new_permissions})",
322 file_path.quote(),
323 );
324 } else if self.verbose {
325 println!(
326 "mode of {} retained as {old_mode:04o} ({current_permissions})",
327 file_path.quote(),
328 );
329 }
330 }
331 }
332
333 #[cfg(not(unix))]
335 fn handle_symlink_during_traversal(
336 &self,
337 path: &Path,
338 is_command_line_arg: bool,
339 ) -> UResult<()> {
340 let should_follow_symlink = match self.traverse_symlinks {
341 TraverseSymlinks::All => true,
342 TraverseSymlinks::First => is_command_line_arg,
343 TraverseSymlinks::None => false,
344 };
345
346 if !should_follow_symlink {
347 return self.chmod_file_internal(path, false);
348 }
349
350 match fs::metadata(path) {
351 Ok(meta) if meta.is_dir() => self.walk_dir_with_context(path, false),
352 Ok(_) => {
353 self.chmod_file(path)
355 }
356 Err(_) => {
357 self.chmod_file_internal(path, false)
359 }
360 }
361 }
362
363 fn chmod(&self, files: &[OsString]) -> UResult<()> {
364 let mut r = Ok(());
365
366 for filename in files {
367 let file = Path::new(filename);
368 if !file.exists() {
369 if file.is_symlink() {
370 if !self.dereference && !self.recursive {
371 continue;
374 }
375 if self.recursive && self.traverse_symlinks == TraverseSymlinks::None {
376 continue;
377 }
378
379 if !self.quiet {
380 show!(ChmodError::DanglingSymlink(filename.into()));
381 set_exit_code(1);
382 }
383
384 if self.verbose {
385 println!(
386 "{}",
387 translate!("chmod-verbose-failed-dangling", "file" => filename.quote())
388 );
389 }
390 } else if !self.quiet {
391 show!(ChmodError::NoSuchFile(filename.into()));
392 }
393 set_exit_code(1);
396 continue;
397 } else if !self.dereference && file.is_symlink() {
398 continue;
402 }
403 if self.recursive && self.preserve_root && Self::is_root(file) {
404 return Err(ChmodError::PreserveRoot("/".into()).into());
405 }
406 if self.recursive {
407 r = self.walk_dir_with_context(file, true).and(r);
408 } else {
409 r = self.chmod_file(file).and(r);
410 }
411 }
412 r
413 }
414
415 fn is_root(file: impl AsRef<Path>) -> bool {
416 matches!(fs::canonicalize(&file), Ok(p) if p == Path::new("/"))
417 }
418
419 #[cfg(any(not(unix), target_os = "redox"))]
421 fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> {
422 let mut r = self.chmod_file(file_path);
423
424 let should_follow_symlink = match self.traverse_symlinks {
426 TraverseSymlinks::All => true,
427 TraverseSymlinks::First => is_command_line_arg, TraverseSymlinks::None => false,
429 };
430
431 if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
433 let mut paths_in_this_dir = Vec::new();
435
436 for dir_entry in file_path.read_dir()? {
437 match dir_entry {
438 Ok(entry) => paths_in_this_dir.push(entry.path()),
439 Err(err) => {
440 r = r.and(Err(err.into()));
441 continue;
442 }
443 }
444 }
445 for path in paths_in_this_dir {
446 #[cfg(not(unix))]
447 {
448 if path.is_symlink() {
449 r = self.handle_symlink_during_recursion(&path).and(r);
450 } else {
451 r = self.walk_dir_with_context(path.as_path(), false).and(r);
452 }
453 }
454 #[cfg(target_os = "redox")]
455 {
456 r = self.walk_dir_with_context(path.as_path(), false).and(r);
457 }
458 }
459 }
460 r
461 }
462
463 #[cfg(all(unix, not(target_os = "redox")))]
464 fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> {
465 let mut r = self.chmod_file(file_path);
466
467 let should_follow_symlink = match self.traverse_symlinks {
469 TraverseSymlinks::All => true,
470 TraverseSymlinks::First => is_command_line_arg, TraverseSymlinks::None => false,
472 };
473
474 if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
476 match DirFd::open(file_path, SymlinkBehavior::Follow) {
477 Ok(dir_fd) => {
478 r = self.safe_traverse_dir(&dir_fd, file_path).and(r);
479 }
480 Err(err) => {
481 if err.kind() == std::io::ErrorKind::PermissionDenied {
483 r = r.and(Err(ChmodError::PermissionDenied(file_path.into()).into()));
484 } else {
485 r = r.and(Err(err.into()));
486 }
487 }
488 }
489 }
490 r
491 }
492
493 #[cfg(all(unix, not(target_os = "redox")))]
494 fn safe_traverse_dir(&self, dir_fd: &DirFd, dir_path: &Path) -> UResult<()> {
495 let mut r = Ok(());
496
497 let entries = dir_fd.read_dir()?;
498
499 let should_follow_symlink = self.traverse_symlinks == TraverseSymlinks::All;
501
502 for entry_name in entries {
503 let entry_path = dir_path.join(&entry_name);
504
505 let dir_meta = dir_fd.metadata_at(&entry_name, should_follow_symlink.into());
506 let Ok(meta) = dir_meta else {
507 let e = dir_meta.unwrap_err();
509 let error = if e.kind() == std::io::ErrorKind::PermissionDenied {
510 ChmodError::PermissionDenied(entry_path).into()
511 } else {
512 e.into()
513 };
514 r = r.and(Err(error));
515 continue;
516 };
517
518 if entry_path.is_symlink() {
519 r = self
520 .handle_symlink_during_safe_recursion(&entry_path, dir_fd, &entry_name)
521 .and(r);
522 } else {
523 r = self
525 .safe_chmod_file(&entry_path, dir_fd, &entry_name, meta.mode() & 0o7777)
526 .and(r);
527
528 if meta.is_dir() {
530 match dir_fd.open_subdir(&entry_name, SymlinkBehavior::Follow) {
531 Ok(child_dir_fd) => {
532 r = self.safe_traverse_dir(&child_dir_fd, &entry_path).and(r);
533 }
534 Err(err) => {
535 let error = if err.kind() == std::io::ErrorKind::PermissionDenied {
536 ChmodError::PermissionDenied(entry_path).into()
537 } else {
538 err.into()
539 };
540 r = r.and(Err(error));
541 }
542 }
543 }
544 }
545 }
546 r
547 }
548
549 #[cfg(all(unix, not(target_os = "redox")))]
550 fn handle_symlink_during_safe_recursion(
551 &self,
552 path: &Path,
553 dir_fd: &DirFd,
554 entry_name: &std::ffi::OsStr,
555 ) -> UResult<()> {
556 match self.traverse_symlinks {
558 TraverseSymlinks::All => {
559 match fs::metadata(path) {
562 Ok(meta) if meta.is_dir() => self.walk_dir_with_context(path, false),
563 Ok(meta) => {
564 self.safe_chmod_file(path, dir_fd, entry_name, meta.mode() & 0o7777)
566 }
567 Err(_) => {
568 self.chmod_file_internal(path, false)
570 }
571 }
572 }
573 TraverseSymlinks::First | TraverseSymlinks::None => {
574 self.chmod_file_internal(path, false)
577 }
578 }
579 }
580
581 #[cfg(all(unix, not(target_os = "redox")))]
582 fn safe_chmod_file(
583 &self,
584 file_path: &Path,
585 dir_fd: &DirFd,
586 entry_name: &std::ffi::OsStr,
587 current_mode: u32,
588 ) -> UResult<()> {
589 let (new_mode, _) = self.calculate_new_mode(current_mode, file_path.is_dir())?;
591
592 let follow_symlinks = self.dereference;
594 if let Err(_e) = dir_fd.chmod_at(entry_name, new_mode, follow_symlinks.into()) {
595 if self.verbose {
596 println!(
597 "failed to change mode of {} to {new_mode:o}",
598 file_path.quote(),
599 );
600 }
601 return Err(ChmodError::PermissionDenied(file_path.into()).into());
602 }
603
604 self.report_permission_change(file_path, current_mode, new_mode);
606
607 Ok(())
608 }
609
610 #[cfg(not(unix))]
611 fn handle_symlink_during_recursion(&self, path: &Path) -> UResult<()> {
612 self.handle_symlink_during_traversal(path, false)
614 }
615
616 fn chmod_file(&self, file: &Path) -> UResult<()> {
617 self.chmod_file_internal(file, self.dereference)
618 }
619
620 fn chmod_file_internal(&self, file: &Path, dereference: bool) -> UResult<()> {
621 use uucore::perms::get_metadata;
622
623 let metadata = get_metadata(file, dereference);
624
625 let fperm = match metadata {
626 Ok(meta) => meta.mode() & 0o7777,
627 Err(err) => {
628 return if file.is_symlink() && !dereference {
630 if self.verbose {
631 println!(
632 "neither symbolic link {} nor referent has been changed",
633 file.quote()
634 );
635 }
636 Ok(()) } else if err.kind() == std::io::ErrorKind::PermissionDenied {
638 Err(ChmodError::PermissionDenied(file.into()).into())
639 } else {
640 Err(ChmodError::CannotStat(file.into()).into())
641 };
642 }
643 };
644
645 let (new_mode, naively_expected_new_mode) =
647 self.calculate_new_mode(fperm, file.is_dir())?;
648
649 if let Some(mode) = self.fmode {
651 self.change_file(fperm, mode, file)?;
652 } else {
653 if file.is_symlink() && !dereference {
655 if self.verbose {
659 println!(
660 "neither symbolic link {} nor referent has been changed",
661 file.quote()
662 );
663 }
664 } else {
665 self.change_file(fperm, new_mode, file)?;
666 }
667 if (new_mode & !naively_expected_new_mode) != 0 {
669 return Err(ChmodError::NewPermissions(
670 file.into(),
671 display_permissions_unix(new_mode as mode_t, false),
672 display_permissions_unix(naively_expected_new_mode as mode_t, false),
673 )
674 .into());
675 }
676 }
677
678 Ok(())
679 }
680
681 fn change_file(&self, fperm: u32, mode: u32, file: &Path) -> Result<(), i32> {
682 if fperm == mode {
683 self.report_permission_change(file, fperm, mode);
685 Ok(())
686 } else if let Err(err) = fs::set_permissions(file, fs::Permissions::from_mode(mode)) {
687 if !self.quiet {
688 show_error!("{err}");
689 }
690 if self.verbose {
691 println!(
692 "failed to change mode of file {} from {fperm:04o} ({}) to {mode:04o} ({})",
693 file.quote(),
694 display_permissions_unix(fperm as mode_t, false),
695 display_permissions_unix(mode as mode_t, false)
696 );
697 }
698 Err(1)
699 } else {
700 self.report_permission_change(file, fperm, mode);
702 Ok(())
703 }
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
712 fn test_extract_negative_modes() {
713 let (c, a) = extract_negative_modes(["-w", "-r", "file"].iter().map(OsString::from));
716 assert_eq!(c, Some("-w,-r".to_string()));
717 assert_eq!(a, ["w", "file"]);
718
719 let (c, a) = extract_negative_modes(["-w", "file", "-r"].iter().map(OsString::from));
722 assert_eq!(c, Some("-w,-r".to_string()));
723 assert_eq!(a, ["w", "file"]);
724
725 let (c, a) = extract_negative_modes(["-w", "--", "-r", "f"].iter().map(OsString::from));
728 assert_eq!(c, Some("-w".to_string()));
729 assert_eq!(a, ["w", "--", "-r", "f"]);
730
731 let (c, a) = extract_negative_modes(["--", "-r", "file"].iter().map(OsString::from));
733 assert_eq!(c, None);
734 assert_eq!(a, ["--", "-r", "file"]);
735 }
736}