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
62fn parse_symbolic_mode(mode_str: &str, current_mode: u32) -> Result<u32, String> {
66 let mut mode = current_mode & 0o7777;
67
68 let file_type_bits = current_mode & 0o170000;
71
72 let umask = get_umask();
74
75 for clause in mode_str.split(',') {
76 if clause.is_empty() {
77 return Err(format!("invalid mode: '{}'", mode_str));
78 }
79 mode = apply_symbolic_clause(clause, mode | file_type_bits, umask)? & 0o7777;
80 }
81
82 Ok(mode)
83}
84
85fn get_umask() -> u32 {
87 let old = unsafe { libc::umask(0) };
90 unsafe {
91 libc::umask(old);
92 }
93 old as u32
94}
95
96fn apply_symbolic_clause(clause: &str, current_mode: u32, umask: u32) -> Result<u32, String> {
98 let bytes = clause.as_bytes();
99 let len = bytes.len();
100 let mut pos = 0;
101
102 let mut who_mask: u32 = 0;
104 let mut who_specified = false;
105 while pos < len {
106 match bytes[pos] {
107 b'u' => {
108 who_mask |= USER_BITS | S_ISUID;
109 who_specified = true;
110 }
111 b'g' => {
112 who_mask |= GROUP_BITS | S_ISGID;
113 who_specified = true;
114 }
115 b'o' => {
116 who_mask |= OTHER_BITS | S_ISVTX;
117 who_specified = true;
118 }
119 b'a' => {
120 who_mask |= ALL_BITS;
121 who_specified = true;
122 }
123 _ => break,
124 }
125 pos += 1;
126 }
127
128 if !who_specified {
130 who_mask = ALL_BITS;
131 }
132
133 if pos >= len {
134 return Err(format!("invalid mode: '{}'", clause));
135 }
136
137 let mut mode = current_mode;
138
139 while pos < len {
141 let op = match bytes[pos] {
143 b'+' => '+',
144 b'-' => '-',
145 b'=' => '=',
146 _ => return Err(format!("invalid mode: '{}'", clause)),
147 };
148 pos += 1;
149
150 let mut perm_bits: u32 = 0;
152 let mut has_x_cap = false;
153
154 while pos < len && bytes[pos] != b'+' && bytes[pos] != b'-' && bytes[pos] != b'=' {
155 match bytes[pos] {
156 b'r' => {
157 perm_bits |= S_IRUSR | S_IRGRP | S_IROTH;
158 }
159 b'w' => {
160 perm_bits |= S_IWUSR | S_IWGRP | S_IWOTH;
161 }
162 b'x' => {
163 perm_bits |= S_IXUSR | S_IXGRP | S_IXOTH;
164 }
165 b'X' => {
166 has_x_cap = true;
167 }
168 b's' => {
169 perm_bits |= S_ISUID | S_ISGID;
170 }
171 b't' => {
172 perm_bits |= S_ISVTX;
173 }
174 b'u' => {
175 let u = current_mode & USER_BITS;
177 perm_bits |= u | (u >> 3) | (u >> 6);
178 }
179 b'g' => {
180 let g = current_mode & GROUP_BITS;
182 perm_bits |= (g << 3) | g | (g >> 3);
183 }
184 b'o' => {
185 let o = current_mode & OTHER_BITS;
187 perm_bits |= (o << 6) | (o << 3) | o;
188 }
189 b',' => break,
190 _ => return Err(format!("invalid mode: '{}'", clause)),
191 }
192 pos += 1;
193 }
194
195 if has_x_cap {
197 let is_executable = (current_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0;
201 let is_dir = (current_mode & 0o170000) == 0o040000;
203 if is_executable || is_dir {
204 perm_bits |= S_IXUSR | S_IXGRP | S_IXOTH;
205 }
206 }
207
208 let effective = perm_bits & who_mask;
210
211 let effective = if !who_specified {
213 let umask_filter = !(umask) & (USER_BITS | GROUP_BITS | OTHER_BITS);
216 let special = effective & (S_ISUID | S_ISGID | S_ISVTX);
218 (effective & umask_filter) | special
219 } else {
220 effective
221 };
222
223 match op {
224 '+' => {
225 mode |= effective;
226 }
227 '-' => {
228 mode &= !effective;
229 }
230 '=' => {
231 let clear_mask = who_mask & (USER_BITS | GROUP_BITS | OTHER_BITS);
233 let clear_special = who_mask & (S_ISUID | S_ISGID | S_ISVTX);
235 mode &= !(clear_mask | clear_special);
236
237 let effective_eq = if !who_specified {
238 let umask_filter = !(umask) & (USER_BITS | GROUP_BITS | OTHER_BITS);
239 let special = (perm_bits & who_mask) & (S_ISUID | S_ISGID | S_ISVTX);
240 ((perm_bits & who_mask) & umask_filter) | special
241 } else {
242 perm_bits & who_mask
243 };
244 mode |= effective_eq;
245 }
246 _ => unreachable!(),
247 }
248 }
249
250 Ok(mode)
251}
252
253fn format_mode(mode: u32) -> String {
255 format!("{:04o}", mode & 0o7777)
256}
257
258pub fn chmod_file(path: &Path, mode: u32, config: &ChmodConfig) -> Result<bool, io::Error> {
263 let metadata = fs::symlink_metadata(path)?;
264
265 if metadata.file_type().is_symlink() {
267 return Ok(false);
268 }
269
270 let old_mode = metadata.mode() & 0o7777;
271 let changed = old_mode != mode;
272
273 if changed {
274 let perms = fs::Permissions::from_mode(mode);
275 fs::set_permissions(path, perms)?;
276 }
277
278 let path_display = path.display();
279 if config.verbose {
280 if changed {
281 eprintln!(
282 "mode of '{}' changed from {} to {}",
283 path_display,
284 format_mode(old_mode),
285 format_mode(mode)
286 );
287 } else {
288 eprintln!(
289 "mode of '{}' retained as {}",
290 path_display,
291 format_mode(old_mode)
292 );
293 }
294 } else if config.changes && changed {
295 eprintln!(
296 "mode of '{}' changed from {} to {}",
297 path_display,
298 format_mode(old_mode),
299 format_mode(mode)
300 );
301 }
302
303 Ok(changed)
304}
305
306pub fn chmod_recursive(
311 path: &Path,
312 mode_str: &str,
313 config: &ChmodConfig,
314) -> Result<bool, io::Error> {
315 if config.preserve_root && path == Path::new("/") {
316 return Err(io::Error::other(
317 "it is dangerous to operate recursively on '/'",
318 ));
319 }
320
321 let mut had_error = false;
322
323 match process_entry(path, mode_str, config) {
325 Ok(()) => {}
326 Err(e) => {
327 if !config.quiet {
328 eprintln!("chmod: cannot access '{}': {}", path.display(), e);
329 }
330 had_error = true;
331 }
332 }
333
334 if path.is_dir() {
336 walk_dir(path, mode_str, config, &mut had_error);
337 }
338
339 if had_error {
340 Err(io::Error::other("some operations failed"))
341 } else {
342 Ok(true)
343 }
344}
345
346fn process_entry(path: &Path, mode_str: &str, config: &ChmodConfig) -> Result<(), io::Error> {
348 let metadata = fs::symlink_metadata(path)?;
349
350 if metadata.file_type().is_symlink() {
352 return Ok(());
353 }
354
355 let current_mode = metadata.mode();
356 let new_mode = parse_mode(mode_str, current_mode).map_err(|e| io::Error::other(e))?;
357 chmod_file(path, new_mode, config)?;
358 Ok(())
359}
360
361fn walk_dir(dir: &Path, mode_str: &str, config: &ChmodConfig, had_error: &mut bool) {
364 if !config.verbose && !config.changes {
366 let error_flag = std::sync::atomic::AtomicBool::new(false);
367 walk_dir_parallel(dir, mode_str, config, &error_flag);
368 if error_flag.load(std::sync::atomic::Ordering::Relaxed) {
369 *had_error = true;
370 }
371 return;
372 }
373
374 let entries = match fs::read_dir(dir) {
376 Ok(entries) => entries,
377 Err(e) => {
378 if !config.quiet {
379 eprintln!("chmod: cannot open directory '{}': {}", dir.display(), e);
380 }
381 *had_error = true;
382 return;
383 }
384 };
385
386 for entry in entries {
387 let entry = match entry {
388 Ok(e) => e,
389 Err(e) => {
390 if !config.quiet {
391 eprintln!("chmod: error reading directory entry: {}", e);
392 }
393 *had_error = true;
394 continue;
395 }
396 };
397
398 let entry_path = entry.path();
399
400 let file_type = match entry.file_type() {
401 Ok(ft) => ft,
402 Err(e) => {
403 if !config.quiet {
404 eprintln!(
405 "chmod: cannot read file type of '{}': {}",
406 entry_path.display(),
407 e
408 );
409 }
410 *had_error = true;
411 continue;
412 }
413 };
414
415 if file_type.is_symlink() {
416 continue;
417 }
418
419 match process_entry(&entry_path, mode_str, config) {
420 Ok(()) => {}
421 Err(e) => {
422 if !config.quiet {
423 eprintln!(
424 "chmod: changing permissions of '{}': {}",
425 entry_path.display(),
426 e
427 );
428 }
429 *had_error = true;
430 }
431 }
432
433 if file_type.is_dir() {
434 walk_dir(&entry_path, mode_str, config, had_error);
435 }
436 }
437}
438
439fn walk_dir_parallel(
441 dir: &Path,
442 mode_str: &str,
443 config: &ChmodConfig,
444 had_error: &std::sync::atomic::AtomicBool,
445) {
446 let entries = match fs::read_dir(dir) {
447 Ok(entries) => entries,
448 Err(e) => {
449 if !config.quiet {
450 eprintln!("chmod: cannot open directory '{}': {}", dir.display(), e);
451 }
452 had_error.store(true, std::sync::atomic::Ordering::Relaxed);
453 return;
454 }
455 };
456
457 let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
458
459 use rayon::prelude::*;
460 entries.par_iter().for_each(|entry| {
461 let entry_path = entry.path();
462 let file_type = match entry.file_type() {
463 Ok(ft) => ft,
464 Err(_) => {
465 had_error.store(true, std::sync::atomic::Ordering::Relaxed);
466 return;
467 }
468 };
469
470 if file_type.is_symlink() {
471 return;
472 }
473
474 if process_entry(&entry_path, mode_str, config).is_err() {
475 had_error.store(true, std::sync::atomic::Ordering::Relaxed);
476 }
477
478 if file_type.is_dir() {
479 walk_dir_parallel(&entry_path, mode_str, config, had_error);
480 }
481 });
482}