@@ -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;
@@ -322,6 +325,7 @@ impl Chmoder {
322325 r
323326 }
324327
328+ #[ cfg( not( target_os = "linux" ) ) ]
325329 fn walk_dir_with_context ( & self , file_path : & Path , is_command_line_arg : bool ) -> UResult < ( ) > {
326330 let mut r = self . chmod_file ( file_path) ;
327331
@@ -352,6 +356,213 @@ impl Chmoder {
352356 r
353357 }
354358
359+ #[ cfg( target_os = "linux" ) ]
360+ fn walk_dir_with_context ( & self , file_path : & Path , is_command_line_arg : bool ) -> UResult < ( ) > {
361+ let mut r = self . chmod_file ( file_path) ;
362+
363+ // Determine whether to traverse symlinks based on context and traversal mode
364+ let should_follow_symlink = match self . traverse_symlinks {
365+ TraverseSymlinks :: All => true ,
366+ TraverseSymlinks :: First => is_command_line_arg, // Only follow symlinks that are command line args
367+ TraverseSymlinks :: None => false ,
368+ } ;
369+
370+ // If the path is a directory (or we should follow symlinks), recurse into it using safe traversal
371+ if ( !file_path. is_symlink ( ) || should_follow_symlink) && file_path. is_dir ( ) {
372+ match DirFd :: open ( file_path) {
373+ Ok ( dir_fd) => {
374+ r = self
375+ . safe_traverse_dir ( & dir_fd, file_path, is_command_line_arg)
376+ . and ( r) ;
377+ }
378+ Err ( err) => {
379+ // Handle permission denied errors with proper file path context
380+ if err. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
381+ r = r. and ( Err ( ChmodError :: PermissionDenied (
382+ file_path. to_string_lossy ( ) . to_string ( ) ,
383+ )
384+ . into ( ) ) ) ;
385+ } else {
386+ r = r. and ( Err ( err. into ( ) ) ) ;
387+ }
388+ }
389+ }
390+ }
391+ r
392+ }
393+
394+ #[ cfg( target_os = "linux" ) ]
395+ fn safe_traverse_dir (
396+ & self ,
397+ dir_fd : & DirFd ,
398+ dir_path : & Path ,
399+ _is_command_line_arg : bool ,
400+ ) -> UResult < ( ) > {
401+ let mut r = Ok ( ( ) ) ;
402+
403+ // Read directory entries
404+ let entries = match dir_fd. read_dir ( ) {
405+ Ok ( entries) => entries,
406+ Err ( e) => {
407+ return Err ( e. into ( ) ) ;
408+ }
409+ } ;
410+
411+ for entry_name in entries {
412+ let entry_path = dir_path. join ( & entry_name) ;
413+
414+ // Get metadata for the entry
415+ let follow = self . traverse_symlinks == TraverseSymlinks :: All ;
416+
417+ let meta = match dir_fd. metadata_at ( & entry_name, follow) {
418+ Ok ( m) => m,
419+ Err ( e) => {
420+ // Handle permission denied with proper file path context
421+ if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
422+ r = r. and ( Err ( ChmodError :: PermissionDenied (
423+ entry_path. to_string_lossy ( ) . to_string ( ) ,
424+ )
425+ . into ( ) ) ) ;
426+ } else {
427+ r = r. and ( Err ( e. into ( ) ) ) ;
428+ }
429+ continue ;
430+ }
431+ } ;
432+
433+ if entry_path. is_symlink ( ) {
434+ r = self
435+ . handle_symlink_during_safe_recursion ( & entry_path, dir_fd, & entry_name)
436+ . and ( r) ;
437+ } else {
438+ // For regular files and directories, chmod them
439+ r = self
440+ . safe_chmod_file ( & entry_path, dir_fd, & entry_name, meta. mode ( ) & 0o7777 )
441+ . and ( r) ;
442+
443+ // Recurse into subdirectories
444+ if meta. is_dir ( ) {
445+ r = self . walk_dir_with_context ( & entry_path, false ) . and ( r) ;
446+ }
447+ }
448+ }
449+ r
450+ }
451+
452+ #[ cfg( target_os = "linux" ) ]
453+ fn handle_symlink_during_safe_recursion (
454+ & self ,
455+ path : & Path ,
456+ dir_fd : & DirFd ,
457+ entry_name : & std:: ffi:: OsStr ,
458+ ) -> UResult < ( ) > {
459+ // During recursion, determine behavior based on traversal mode
460+ match self . traverse_symlinks {
461+ TraverseSymlinks :: All => {
462+ // Follow all symlinks during recursion
463+ // Check if the symlink target is a directory, but handle dangling symlinks gracefully
464+ match fs:: metadata ( path) {
465+ Ok ( meta) if meta. is_dir ( ) => self . walk_dir_with_context ( path, false ) ,
466+ Ok ( meta) => {
467+ // It's a file symlink, chmod it using safe traversal
468+ self . safe_chmod_file ( path, dir_fd, entry_name, meta. mode ( ) & 0o7777 )
469+ }
470+ Err ( _) => {
471+ // Dangling symlink, chmod it without dereferencing
472+ self . chmod_file_internal ( path, false )
473+ }
474+ }
475+ }
476+ TraverseSymlinks :: First | TraverseSymlinks :: None => {
477+ // Don't follow symlinks encountered during recursion
478+ // For these symlinks, don't dereference them even if dereference is normally true
479+ self . chmod_file_internal ( path, false )
480+ }
481+ }
482+ }
483+
484+ #[ cfg( target_os = "linux" ) ]
485+ fn safe_chmod_file (
486+ & self ,
487+ file_path : & Path ,
488+ dir_fd : & DirFd ,
489+ entry_name : & std:: ffi:: OsStr ,
490+ current_mode : u32 ,
491+ ) -> UResult < ( ) > {
492+ // Determine the new permissions to apply
493+ let new_mode = match self . fmode {
494+ Some ( mode) => mode,
495+ None => {
496+ let cmode_unwrapped = self . cmode . clone ( ) . unwrap ( ) ;
497+ let mut new_mode = current_mode;
498+ for mode in cmode_unwrapped. split ( ',' ) {
499+ let result = if mode. chars ( ) . any ( |c| c. is_ascii_digit ( ) ) {
500+ mode:: parse_numeric ( new_mode, mode, file_path. is_dir ( ) )
501+ } else {
502+ mode:: parse_symbolic ( new_mode, mode, mode:: get_umask ( ) , file_path. is_dir ( ) )
503+ } ;
504+
505+ match result {
506+ Ok ( parsed_mode) => {
507+ new_mode = parsed_mode;
508+ }
509+ Err ( f) => {
510+ return if self . quiet {
511+ Err ( ExitCode :: new ( 1 ) )
512+ } else {
513+ Err ( USimpleError :: new ( 1 , f) )
514+ } ;
515+ }
516+ }
517+ }
518+ new_mode
519+ }
520+ } ;
521+
522+ // Use safe traversal to change the mode
523+ let follow_symlinks = self . dereference ;
524+ if let Err ( _e) = dir_fd. chmod_at ( entry_name, new_mode, follow_symlinks) {
525+ if self . verbose {
526+ println ! (
527+ "failed to change mode of {} to {:o}" ,
528+ file_path. quote( ) ,
529+ new_mode
530+ ) ;
531+ }
532+ return Err (
533+ ChmodError :: PermissionDenied ( file_path. to_string_lossy ( ) . to_string ( ) ) . into ( ) ,
534+ ) ;
535+ }
536+
537+ // Report the change if verbose or changes mode
538+ if self . verbose || self . changes {
539+ let current_permissions = display_permissions_unix ( current_mode, false ) ;
540+ let new_permissions = display_permissions_unix ( new_mode, false ) ;
541+ if new_mode != current_mode {
542+ if self . changes || self . verbose {
543+ println ! (
544+ "mode of {} changed from {} ({:04o}) to {} ({:04o})" ,
545+ file_path. quote( ) ,
546+ current_permissions,
547+ current_mode,
548+ new_permissions,
549+ new_mode
550+ ) ;
551+ }
552+ } else if self . verbose {
553+ println ! (
554+ "mode of {} retained as {} ({:04o})" ,
555+ file_path. quote( ) ,
556+ current_permissions,
557+ current_mode
558+ ) ;
559+ }
560+ }
561+
562+ Ok ( ( ) )
563+ }
564+
565+ #[ cfg( not( target_os = "linux" ) ) ]
355566 fn handle_symlink_during_recursion ( & self , path : & Path ) -> UResult < ( ) > {
356567 // During recursion, determine behavior based on traversal mode
357568 match self . traverse_symlinks {
0 commit comments