@@ -17,6 +17,9 @@ use uucore::fs::display_permissions_unix;
1717use uucore:: libc:: mode_t;
1818use uucore:: mode;
1919use uucore:: perms:: { TraverseSymlinks , configure_symlink_and_recursion} ;
20+
21+ #[ cfg( target_os = "linux" ) ]
22+ use uucore:: safe_traversal:: DirFd ;
2023use uucore:: { format_usage, show, show_error} ;
2124
2225use uucore:: translate;
@@ -266,6 +269,104 @@ struct Chmoder {
266269}
267270
268271impl Chmoder {
272+ /// Calculate the new mode based on the current mode and the chmod specification.
273+ /// Returns (`new_mode`, `naively_expected_new_mode`) for symbolic modes, or (`new_mode`, `new_mode`) for numeric/reference modes.
274+ fn calculate_new_mode ( & self , current_mode : u32 , is_dir : bool ) -> UResult < ( u32 , u32 ) > {
275+ match self . fmode {
276+ Some ( mode) => Ok ( ( mode, mode) ) ,
277+ None => {
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+ // calculate the new mode as if umask was 0
288+ let naive_mode =
289+ mode:: parse_symbolic ( naively_expected_new_mode, mode, 0 , is_dir)
290+ . unwrap ( ) ; // we know that mode must be valid, so this cannot fail
291+ ( 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+
314+ /// Report permission changes based on verbose and changes flags
315+ fn report_permission_change ( & self , file_path : & Path , old_mode : u32 , new_mode : u32 ) {
316+ if self . verbose || self . changes {
317+ let current_permissions = display_permissions_unix ( old_mode as mode_t , false ) ;
318+ let new_permissions = display_permissions_unix ( new_mode as mode_t , false ) ;
319+
320+ if new_mode != old_mode {
321+ println ! (
322+ "mode of {} changed from {:04o} ({}) to {:04o} ({})" ,
323+ file_path. quote( ) ,
324+ old_mode,
325+ current_permissions,
326+ new_mode,
327+ new_permissions
328+ ) ;
329+ } else if self . verbose {
330+ println ! (
331+ "mode of {} retained as {:04o} ({})" ,
332+ file_path. quote( ) ,
333+ old_mode,
334+ current_permissions
335+ ) ;
336+ }
337+ }
338+ }
339+
340+ /// Handle symlinks during directory traversal based on traversal mode
341+ #[ cfg( not( target_os = "linux" ) ) ]
342+ fn handle_symlink_during_traversal (
343+ & self ,
344+ path : & Path ,
345+ is_command_line_arg : bool ,
346+ ) -> UResult < ( ) > {
347+ let should_follow_symlink = match self . traverse_symlinks {
348+ TraverseSymlinks :: All => true ,
349+ TraverseSymlinks :: First => is_command_line_arg,
350+ TraverseSymlinks :: None => false ,
351+ } ;
352+
353+ if !should_follow_symlink {
354+ return self . chmod_file_internal ( path, false ) ;
355+ }
356+
357+ match fs:: metadata ( path) {
358+ Ok ( meta) if meta. is_dir ( ) => self . walk_dir_with_context ( path, false ) ,
359+ Ok ( _) => {
360+ // It's a file symlink, chmod it
361+ self . chmod_file ( path)
362+ }
363+ Err ( _) => {
364+ // Dangling symlink, chmod it without dereferencing
365+ self . chmod_file_internal ( path, false )
366+ }
367+ }
368+ }
369+
269370 fn chmod ( & self , files : & [ OsString ] ) -> UResult < ( ) > {
270371 let mut r = Ok ( ( ) ) ;
271372
@@ -322,6 +423,7 @@ impl Chmoder {
322423 r
323424 }
324425
426+ #[ cfg( not( target_os = "linux" ) ) ]
325427 fn walk_dir_with_context ( & self , file_path : & Path , is_command_line_arg : bool ) -> UResult < ( ) > {
326428 let mut r = self . chmod_file ( file_path) ;
327429
@@ -352,17 +454,100 @@ impl Chmoder {
352454 r
353455 }
354456
355- fn handle_symlink_during_recursion ( & self , path : & Path ) -> UResult < ( ) > {
457+ #[ cfg( target_os = "linux" ) ]
458+ fn walk_dir_with_context ( & self , file_path : & Path , is_command_line_arg : bool ) -> UResult < ( ) > {
459+ let mut r = self . chmod_file ( file_path) ;
460+
461+ // Determine whether to traverse symlinks based on context and traversal mode
462+ let should_follow_symlink = match self . traverse_symlinks {
463+ TraverseSymlinks :: All => true ,
464+ TraverseSymlinks :: First => is_command_line_arg, // Only follow symlinks that are command line args
465+ TraverseSymlinks :: None => false ,
466+ } ;
467+
468+ // If the path is a directory (or we should follow symlinks), recurse into it using safe traversal
469+ if ( !file_path. is_symlink ( ) || should_follow_symlink) && file_path. is_dir ( ) {
470+ match DirFd :: open ( file_path) {
471+ Ok ( dir_fd) => {
472+ r = self . safe_traverse_dir ( & dir_fd, file_path) . and ( r) ;
473+ }
474+ Err ( err) => {
475+ // Handle permission denied errors with proper file path context
476+ if err. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
477+ r = r. and ( Err ( ChmodError :: PermissionDenied (
478+ file_path. to_string_lossy ( ) . to_string ( ) ,
479+ )
480+ . into ( ) ) ) ;
481+ } else {
482+ r = r. and ( Err ( err. into ( ) ) ) ;
483+ }
484+ }
485+ }
486+ }
487+ r
488+ }
489+
490+ #[ cfg( target_os = "linux" ) ]
491+ fn safe_traverse_dir ( & self , dir_fd : & DirFd , dir_path : & Path ) -> UResult < ( ) > {
492+ let mut r = Ok ( ( ) ) ;
493+
494+ let entries = dir_fd. read_dir ( ) ?;
495+
496+ // Determine if we should follow symlinks (doesn't depend on entry_name)
497+ let should_follow_symlink = self . traverse_symlinks == TraverseSymlinks :: All ;
498+
499+ for entry_name in entries {
500+ let entry_path = dir_path. join ( & entry_name) ;
501+
502+ let dir_meta = dir_fd. metadata_at ( & entry_name, should_follow_symlink) ;
503+ let Ok ( meta) = dir_meta else {
504+ // Handle permission denied with proper file path context
505+ let e = dir_meta. unwrap_err ( ) ;
506+ let error = if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
507+ ChmodError :: PermissionDenied ( entry_path. to_string_lossy ( ) . to_string ( ) ) . into ( )
508+ } else {
509+ e. into ( )
510+ } ;
511+ r = r. and ( Err ( error) ) ;
512+ continue ;
513+ } ;
514+
515+ if entry_path. is_symlink ( ) {
516+ r = self
517+ . handle_symlink_during_safe_recursion ( & entry_path, dir_fd, & entry_name)
518+ . and ( r) ;
519+ } else {
520+ // For regular files and directories, chmod them
521+ r = self
522+ . safe_chmod_file ( & entry_path, dir_fd, & entry_name, meta. mode ( ) & 0o7777 )
523+ . and ( r) ;
524+
525+ // Recurse into subdirectories
526+ if meta. is_dir ( ) {
527+ r = self . walk_dir_with_context ( & entry_path, false ) . and ( r) ;
528+ }
529+ }
530+ }
531+ r
532+ }
533+
534+ #[ cfg( target_os = "linux" ) ]
535+ fn handle_symlink_during_safe_recursion (
536+ & self ,
537+ path : & Path ,
538+ dir_fd : & DirFd ,
539+ entry_name : & std:: ffi:: OsStr ,
540+ ) -> UResult < ( ) > {
356541 // During recursion, determine behavior based on traversal mode
357542 match self . traverse_symlinks {
358543 TraverseSymlinks :: All => {
359544 // Follow all symlinks during recursion
360545 // Check if the symlink target is a directory, but handle dangling symlinks gracefully
361546 match fs:: metadata ( path) {
362547 Ok ( meta) if meta. is_dir ( ) => self . walk_dir_with_context ( path, false ) ,
363- Ok ( _ ) => {
364- // It's a file symlink, chmod it
365- self . chmod_file ( path)
548+ Ok ( meta ) => {
549+ // It's a file symlink, chmod it using safe traversal
550+ self . safe_chmod_file ( path, dir_fd , entry_name , meta . mode ( ) & 0o7777 )
366551 }
367552 Err ( _) => {
368553 // Dangling symlink, chmod it without dereferencing
@@ -378,12 +563,50 @@ impl Chmoder {
378563 }
379564 }
380565
566+ #[ cfg( target_os = "linux" ) ]
567+ fn safe_chmod_file (
568+ & self ,
569+ file_path : & Path ,
570+ dir_fd : & DirFd ,
571+ entry_name : & std:: ffi:: OsStr ,
572+ current_mode : u32 ,
573+ ) -> UResult < ( ) > {
574+ // Calculate the new mode using the helper method
575+ let ( new_mode, _) = self . calculate_new_mode ( current_mode, file_path. is_dir ( ) ) ?;
576+
577+ // Use safe traversal to change the mode
578+ let follow_symlinks = self . dereference ;
579+ if let Err ( _e) = dir_fd. chmod_at ( entry_name, new_mode, follow_symlinks) {
580+ if self . verbose {
581+ println ! (
582+ "failed to change mode of {} to {:o}" ,
583+ file_path. quote( ) ,
584+ new_mode
585+ ) ;
586+ }
587+ return Err (
588+ ChmodError :: PermissionDenied ( file_path. to_string_lossy ( ) . to_string ( ) ) . into ( ) ,
589+ ) ;
590+ }
591+
592+ // Report the change using the helper method
593+ self . report_permission_change ( file_path, current_mode, new_mode) ;
594+
595+ Ok ( ( ) )
596+ }
597+
598+ #[ cfg( not( target_os = "linux" ) ) ]
599+ fn handle_symlink_during_recursion ( & self , path : & Path ) -> UResult < ( ) > {
600+ // Use the common symlink handling logic
601+ self . handle_symlink_during_traversal ( path, false )
602+ }
603+
381604 fn chmod_file ( & self , file : & Path ) -> UResult < ( ) > {
382605 self . chmod_file_internal ( file, self . dereference )
383606 }
384607
385608 fn chmod_file_internal ( & self , file : & Path , dereference : bool ) -> UResult < ( ) > {
386- use uucore:: { mode :: get_umask , perms:: get_metadata} ;
609+ use uucore:: perms:: get_metadata;
387610
388611 let metadata = get_metadata ( file, dereference) ;
389612
@@ -409,45 +632,14 @@ impl Chmoder {
409632 }
410633 } ;
411634
412- // Determine the new permissions to apply
635+ // Calculate the new mode using the helper method
636+ let ( new_mode, naively_expected_new_mode) =
637+ self . calculate_new_mode ( fperm, file. is_dir ( ) ) ?;
638+
639+ // Determine how to apply the permissions
413640 match self . fmode {
414641 Some ( mode) => self . change_file ( fperm, mode, file) ?,
415642 None => {
416- let cmode_unwrapped = self . cmode . clone ( ) . unwrap ( ) ;
417- let mut new_mode = fperm;
418- let mut naively_expected_new_mode = new_mode;
419- for mode in cmode_unwrapped. split ( ',' ) {
420- let result = if mode. chars ( ) . any ( |c| c. is_ascii_digit ( ) ) {
421- mode:: parse_numeric ( new_mode, mode, file. is_dir ( ) ) . map ( |v| ( v, v) )
422- } else {
423- mode:: parse_symbolic ( new_mode, mode, get_umask ( ) , file. is_dir ( ) ) . map ( |m| {
424- // calculate the new mode as if umask was 0
425- let naive_mode = mode:: parse_symbolic (
426- naively_expected_new_mode,
427- mode,
428- 0 ,
429- file. is_dir ( ) ,
430- )
431- . unwrap ( ) ; // we know that mode must be valid, so this cannot fail
432- ( m, naive_mode)
433- } )
434- } ;
435-
436- match result {
437- Ok ( ( mode, naive_mode) ) => {
438- new_mode = mode;
439- naively_expected_new_mode = naive_mode;
440- }
441- Err ( f) => {
442- return if self . quiet {
443- Err ( ExitCode :: new ( 1 ) )
444- } else {
445- Err ( USimpleError :: new ( 1 , f) )
446- } ;
447- }
448- }
449- }
450-
451643 // Special handling for symlinks when not dereferencing
452644 if file. is_symlink ( ) && !dereference {
453645 // TODO: On most Unix systems, symlink permissions are ignored by the kernel,
@@ -479,13 +671,8 @@ impl Chmoder {
479671
480672 fn change_file ( & self , fperm : u32 , mode : u32 , file : & Path ) -> Result < ( ) , i32 > {
481673 if fperm == mode {
482- if self . verbose && !self . changes {
483- println ! (
484- "mode of {} retained as {fperm:04o} ({})" ,
485- file. quote( ) ,
486- display_permissions_unix( fperm as mode_t, false ) ,
487- ) ;
488- }
674+ // Use the helper method for consistent reporting
675+ self . report_permission_change ( file, fperm, mode) ;
489676 Ok ( ( ) )
490677 } else if let Err ( err) = fs:: set_permissions ( file, fs:: Permissions :: from_mode ( mode) ) {
491678 if !self . quiet {
@@ -501,14 +688,8 @@ impl Chmoder {
501688 }
502689 Err ( 1 )
503690 } else {
504- if self . verbose || self . changes {
505- println ! (
506- "mode of {} changed from {fperm:04o} ({}) to {mode:04o} ({})" ,
507- file. quote( ) ,
508- display_permissions_unix( fperm as mode_t, false ) ,
509- display_permissions_unix( mode as mode_t, false )
510- ) ;
511- }
691+ // Use the helper method for consistent reporting
692+ self . report_permission_change ( file, fperm, mode) ;
512693 Ok ( ( ) )
513694 }
514695 }
0 commit comments