1use std::fs;
2use std::io;
3use std::os::unix::fs::MetadataExt;
4use std::os::unix::fs::PermissionsExt;
5use std::path::Path;
6
7#[derive(Debug, Clone, Default)]
9pub struct ChmodConfig {
10 pub changes: bool,
12 pub quiet: bool,
14 pub verbose: bool,
16 pub preserve_root: bool,
18 pub recursive: bool,
20}
21
22const S_ISUID: u32 = 0o4000;
24const S_ISGID: u32 = 0o2000;
25const S_ISVTX: u32 = 0o1000;
26
27const S_IRUSR: u32 = 0o0400;
28const S_IWUSR: u32 = 0o0200;
29const S_IXUSR: u32 = 0o0100;
30
31const S_IRGRP: u32 = 0o0040;
32const S_IWGRP: u32 = 0o0020;
33const S_IXGRP: u32 = 0o0010;
34
35const S_IROTH: u32 = 0o0004;
36const S_IWOTH: u32 = 0o0002;
37const S_IXOTH: u32 = 0o0001;
38
39const USER_BITS: u32 = S_IRUSR | S_IWUSR | S_IXUSR;
40const GROUP_BITS: u32 = S_IRGRP | S_IWGRP | S_IXGRP;
41const OTHER_BITS: u32 = S_IROTH | S_IWOTH | S_IXOTH;
42const ALL_BITS: u32 = USER_BITS | GROUP_BITS | OTHER_BITS | S_ISUID | S_ISGID | S_ISVTX;
43
44pub fn parse_mode(mode_str: &str, current_mode: u32) -> Result<u32, String> {
53 if !mode_str.is_empty() && mode_str.chars().all(|c| c.is_ascii_digit() && c < '8') {
55 if let Ok(octal) = u32::from_str_radix(mode_str, 8) {
56 return Ok(octal & 0o7777);
57 }
58 }
59 parse_symbolic_mode(mode_str, current_mode)
60}
61
62pub fn parse_mode_no_umask(mode_str: &str, current_mode: u32) -> Result<u32, String> {
67 if !mode_str.is_empty() && mode_str.chars().all(|c| c.is_ascii_digit() && c < '8') {
69 if let Ok(octal) = u32::from_str_radix(mode_str, 8) {
70 return Ok(octal & 0o7777);
71 }
72 }
73 parse_symbolic_mode_with_umask(mode_str, current_mode, 0)
74}
75
76pub fn parse_mode_check_umask(mode_str: &str, current_mode: u32) -> Result<(u32, bool), String> {
85 if !mode_str.is_empty() && mode_str.chars().all(|c| c.is_ascii_digit() && c < '8') {
87 if let Ok(octal) = u32::from_str_radix(mode_str, 8) {
88 return Ok((octal & 0o7777, false));
89 }
90 }
91
92 let umask = get_umask();
93 let with_umask = parse_symbolic_mode_with_umask(mode_str, current_mode, umask)?;
94 let without_umask = parse_symbolic_mode_with_umask(mode_str, current_mode, 0)?;
95 Ok((with_umask, with_umask != without_umask))
96}
97
98fn parse_symbolic_mode(mode_str: &str, current_mode: u32) -> Result<u32, String> {
102 parse_symbolic_mode_with_umask(mode_str, current_mode, get_umask())
103}
104
105fn parse_symbolic_mode_with_umask(
107 mode_str: &str,
108 current_mode: u32,
109 umask: u32,
110) -> Result<u32, String> {
111 let mut mode = current_mode & 0o7777;
112
113 let file_type_bits = current_mode & 0o170000;
116
117 for clause in mode_str.split(',') {
118 if clause.is_empty() {
119 return Err(format!("invalid mode: '{}'", mode_str));
120 }
121 mode = apply_symbolic_clause(clause, mode | file_type_bits, umask)? & 0o7777;
122 }
123
124 Ok(mode)
125}
126
127pub fn get_umask() -> u32 {
129 let old = unsafe { libc::umask(0) };
132 unsafe {
133 libc::umask(old);
134 }
135 old as u32
136}
137
138fn apply_symbolic_clause(clause: &str, current_mode: u32, umask: u32) -> Result<u32, String> {
140 let bytes = clause.as_bytes();
141 let len = bytes.len();
142 let mut pos = 0;
143
144 let mut who_mask: u32 = 0;
146 let mut who_specified = false;
147 while pos < len {
148 match bytes[pos] {
149 b'u' => {
150 who_mask |= USER_BITS | S_ISUID;
151 who_specified = true;
152 }
153 b'g' => {
154 who_mask |= GROUP_BITS | S_ISGID;
155 who_specified = true;
156 }
157 b'o' => {
158 who_mask |= OTHER_BITS | S_ISVTX;
159 who_specified = true;
160 }
161 b'a' => {
162 who_mask |= ALL_BITS;
163 who_specified = true;
164 }
165 _ => break,
166 }
167 pos += 1;
168 }
169
170 if !who_specified {
172 who_mask = ALL_BITS;
173 }
174
175 if pos >= len {
176 return Err(format!("invalid mode: '{}'", clause));
177 }
178
179 let mut mode = current_mode;
180
181 while pos < len {
183 let op = match bytes[pos] {
185 b'+' => '+',
186 b'-' => '-',
187 b'=' => '=',
188 _ => return Err(format!("invalid mode: '{}'", clause)),
189 };
190 pos += 1;
191
192 let mut perm_bits: u32 = 0;
194 let mut has_x_cap = false;
195 let mut has_perm_chars = false;
198 let mut has_copy_from = false;
199
200 while pos < len && bytes[pos] != b'+' && bytes[pos] != b'-' && bytes[pos] != b'=' {
201 match bytes[pos] {
202 b'r' => {
203 if has_copy_from {
204 return Err(format!("invalid mode: '{}'", clause));
205 }
206 has_perm_chars = true;
207 perm_bits |= S_IRUSR | S_IRGRP | S_IROTH;
208 }
209 b'w' => {
210 if has_copy_from {
211 return Err(format!("invalid mode: '{}'", clause));
212 }
213 has_perm_chars = true;
214 perm_bits |= S_IWUSR | S_IWGRP | S_IWOTH;
215 }
216 b'x' => {
217 if has_copy_from {
218 return Err(format!("invalid mode: '{}'", clause));
219 }
220 has_perm_chars = true;
221 perm_bits |= S_IXUSR | S_IXGRP | S_IXOTH;
222 }
223 b'X' => {
224 if has_copy_from {
225 return Err(format!("invalid mode: '{}'", clause));
226 }
227 has_perm_chars = true;
228 has_x_cap = true;
229 }
230 b's' => {
231 if has_copy_from {
232 return Err(format!("invalid mode: '{}'", clause));
233 }
234 has_perm_chars = true;
235 perm_bits |= S_ISUID | S_ISGID;
236 }
237 b't' => {
238 if has_copy_from {
239 return Err(format!("invalid mode: '{}'", clause));
240 }
241 has_perm_chars = true;
242 perm_bits |= S_ISVTX;
243 }
244 b'u' => {
245 if has_perm_chars {
246 return Err(format!("invalid mode: '{}'", clause));
247 }
248 has_copy_from = true;
249 let u = current_mode & USER_BITS;
251 perm_bits |= u | (u >> 3) | (u >> 6);
252 }
253 b'g' => {
254 if has_perm_chars {
255 return Err(format!("invalid mode: '{}'", clause));
256 }
257 has_copy_from = true;
258 let g = current_mode & GROUP_BITS;
260 perm_bits |= (g << 3) | g | (g >> 3);
261 }
262 b'o' => {
263 if has_perm_chars {
264 return Err(format!("invalid mode: '{}'", clause));
265 }
266 has_copy_from = true;
267 let o = current_mode & OTHER_BITS;
269 perm_bits |= (o << 6) | (o << 3) | o;
270 }
271 b',' => break,
272 _ => return Err(format!("invalid mode: '{}'", clause)),
273 }
274 pos += 1;
275 }
276
277 if has_x_cap {
279 let is_executable = (current_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0;
283 let is_dir = (current_mode & 0o170000) == 0o040000;
285 if is_executable || is_dir {
286 perm_bits |= S_IXUSR | S_IXGRP | S_IXOTH;
287 }
288 }
289
290 let effective = perm_bits & who_mask;
292
293 let effective = if !who_specified {
295 let umask_filter = !(umask) & (USER_BITS | GROUP_BITS | OTHER_BITS);
298 let special = effective & (S_ISUID | S_ISGID | S_ISVTX);
300 (effective & umask_filter) | special
301 } else {
302 effective
303 };
304
305 match op {
306 '+' => {
307 mode |= effective;
308 }
309 '-' => {
310 mode &= !effective;
311 }
312 '=' => {
313 let clear_mask = who_mask & (USER_BITS | GROUP_BITS | OTHER_BITS);
315 let clear_special = who_mask & (S_ISUID | S_ISGID | S_ISVTX);
317 mode &= !(clear_mask | clear_special);
318
319 let effective_eq = if !who_specified {
320 let umask_filter = !(umask) & (USER_BITS | GROUP_BITS | OTHER_BITS);
321 let special = (perm_bits & who_mask) & (S_ISUID | S_ISGID | S_ISVTX);
322 ((perm_bits & who_mask) & umask_filter) | special
323 } else {
324 perm_bits & who_mask
325 };
326 mode |= effective_eq;
327 }
328 _ => unreachable!(),
329 }
330 }
331
332 Ok(mode)
333}
334
335fn format_mode(mode: u32) -> String {
337 format!("{:04o}", mode & 0o7777)
338}
339
340fn format_symbolic(mode: u32) -> String {
343 let m = mode & 0o7777;
344 let mut s = [b'-'; 9];
345
346 if m & S_IRUSR != 0 {
348 s[0] = b'r';
349 }
350 if m & S_IWUSR != 0 {
351 s[1] = b'w';
352 }
353 if m & S_IXUSR != 0 {
354 s[2] = if m & S_ISUID != 0 { b's' } else { b'x' };
355 } else if m & S_ISUID != 0 {
356 s[2] = b'S';
357 }
358
359 if m & S_IRGRP != 0 {
361 s[3] = b'r';
362 }
363 if m & S_IWGRP != 0 {
364 s[4] = b'w';
365 }
366 if m & S_IXGRP != 0 {
367 s[5] = if m & S_ISGID != 0 { b's' } else { b'x' };
368 } else if m & S_ISGID != 0 {
369 s[5] = b'S';
370 }
371
372 if m & S_IROTH != 0 {
374 s[6] = b'r';
375 }
376 if m & S_IWOTH != 0 {
377 s[7] = b'w';
378 }
379 if m & S_IXOTH != 0 {
380 s[8] = if m & S_ISVTX != 0 { b't' } else { b'x' };
381 } else if m & S_ISVTX != 0 {
382 s[8] = b'T';
383 }
384
385 String::from_utf8(s.to_vec()).unwrap()
386}
387
388pub fn format_symbolic_for_warning(mode: u32) -> String {
391 format_symbolic(mode)
392}
393
394pub fn chmod_file(path: &Path, mode: u32, config: &ChmodConfig) -> Result<bool, io::Error> {
401 let metadata = fs::symlink_metadata(path)?;
402
403 if metadata.file_type().is_symlink() {
405 return Ok(false);
406 }
407
408 let old_mode = metadata.mode() & 0o7777;
409 let changed = old_mode != mode;
410
411 if changed {
412 let perms = fs::Permissions::from_mode(mode);
413 fs::set_permissions(path, perms)?;
414 }
415
416 let path_display = path.display();
417 if config.verbose {
418 if changed {
419 println!(
420 "mode of '{}' changed from {} ({}) to {} ({})",
421 path_display,
422 format_mode(old_mode),
423 format_symbolic(old_mode),
424 format_mode(mode),
425 format_symbolic(mode)
426 );
427 } else {
428 println!(
429 "mode of '{}' retained as {} ({})",
430 path_display,
431 format_mode(old_mode),
432 format_symbolic(old_mode)
433 );
434 }
435 } else if config.changes && changed {
436 println!(
437 "mode of '{}' changed from {} ({}) to {} ({})",
438 path_display,
439 format_mode(old_mode),
440 format_symbolic(old_mode),
441 format_mode(mode),
442 format_symbolic(mode)
443 );
444 }
445
446 Ok(changed)
447}
448
449pub fn chmod_recursive(
454 path: &Path,
455 mode_str: &str,
456 config: &ChmodConfig,
457) -> Result<bool, io::Error> {
458 if config.preserve_root && path == Path::new("/") {
459 return Err(io::Error::other(
460 "it is dangerous to operate recursively on '/'",
461 ));
462 }
463
464 let mut had_error = false;
465
466 match process_entry(path, mode_str, config) {
468 Ok(()) => {}
469 Err(e) => {
470 if !config.quiet {
471 eprintln!("chmod: cannot access '{}': {}", path.display(), e);
472 }
473 had_error = true;
474 }
475 }
476
477 if path.is_dir() {
479 walk_dir(path, mode_str, config, &mut had_error);
480 }
481
482 if had_error {
483 Err(io::Error::other("some operations failed"))
484 } else {
485 Ok(true)
486 }
487}
488
489fn process_entry(path: &Path, mode_str: &str, config: &ChmodConfig) -> Result<(), io::Error> {
491 let metadata = fs::symlink_metadata(path)?;
492
493 if metadata.file_type().is_symlink() {
495 return Ok(());
496 }
497
498 let current_mode = metadata.mode();
499 let mut new_mode = parse_mode(mode_str, current_mode).map_err(|e| io::Error::other(e))?;
500
501 if metadata.is_dir()
504 && !mode_str.is_empty()
505 && mode_str.bytes().all(|b| b.is_ascii_digit() && b < b'8')
506 && mode_str.len() <= 4
507 {
508 let existing_special = current_mode & 0o7000;
509 new_mode |= existing_special;
510 }
511
512 chmod_file(path, new_mode, config)?;
513 Ok(())
514}
515
516fn walk_dir(dir: &Path, mode_str: &str, config: &ChmodConfig, had_error: &mut bool) {
519 if !config.verbose && !config.changes {
521 let error_flag = std::sync::atomic::AtomicBool::new(false);
522 walk_dir_parallel(dir, mode_str, config, &error_flag);
523 if error_flag.load(std::sync::atomic::Ordering::Relaxed) {
524 *had_error = true;
525 }
526 return;
527 }
528
529 let entries = match fs::read_dir(dir) {
531 Ok(entries) => entries,
532 Err(e) => {
533 if !config.quiet {
534 eprintln!("chmod: cannot open directory '{}': {}", dir.display(), e);
535 }
536 *had_error = true;
537 return;
538 }
539 };
540
541 for entry in entries {
542 let entry = match entry {
543 Ok(e) => e,
544 Err(e) => {
545 if !config.quiet {
546 eprintln!("chmod: error reading directory entry: {}", e);
547 }
548 *had_error = true;
549 continue;
550 }
551 };
552
553 let entry_path = entry.path();
554
555 let file_type = match entry.file_type() {
556 Ok(ft) => ft,
557 Err(e) => {
558 if !config.quiet {
559 eprintln!(
560 "chmod: cannot read file type of '{}': {}",
561 entry_path.display(),
562 e
563 );
564 }
565 *had_error = true;
566 continue;
567 }
568 };
569
570 if file_type.is_symlink() {
571 continue;
572 }
573
574 match process_entry(&entry_path, mode_str, config) {
575 Ok(()) => {}
576 Err(e) => {
577 if !config.quiet {
578 eprintln!(
579 "chmod: changing permissions of '{}': {}",
580 entry_path.display(),
581 e
582 );
583 }
584 *had_error = true;
585 }
586 }
587
588 if file_type.is_dir() {
589 walk_dir(&entry_path, mode_str, config, had_error);
590 }
591 }
592}
593
594fn walk_dir_parallel(
596 dir: &Path,
597 mode_str: &str,
598 config: &ChmodConfig,
599 had_error: &std::sync::atomic::AtomicBool,
600) {
601 let entries = match fs::read_dir(dir) {
602 Ok(entries) => entries,
603 Err(e) => {
604 if !config.quiet {
605 eprintln!("chmod: cannot open directory '{}': {}", dir.display(), e);
606 }
607 had_error.store(true, std::sync::atomic::Ordering::Relaxed);
608 return;
609 }
610 };
611
612 let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
613
614 use rayon::prelude::*;
615 entries.par_iter().for_each(|entry| {
616 let entry_path = entry.path();
617 let file_type = match entry.file_type() {
618 Ok(ft) => ft,
619 Err(_) => {
620 had_error.store(true, std::sync::atomic::Ordering::Relaxed);
621 return;
622 }
623 };
624
625 if file_type.is_symlink() {
626 return;
627 }
628
629 if process_entry(&entry_path, mode_str, config).is_err() {
630 had_error.store(true, std::sync::atomic::Ordering::Relaxed);
631 }
632
633 if file_type.is_dir() {
634 walk_dir_parallel(&entry_path, mode_str, config, had_error);
635 }
636 });
637}