@@ -19,13 +19,13 @@ use std::io;
1919use std:: os:: unix;
2020#[ cfg( windows) ]
2121use std:: os:: windows;
22- use std:: path:: { Path , PathBuf } ;
22+ use std:: path:: { absolute , Path , PathBuf } ;
2323use uucore:: backup_control:: { self , source_is_target_backup} ;
2424use uucore:: display:: Quotable ;
2525use uucore:: error:: { set_exit_code, FromIo , UResult , USimpleError , UUsageError } ;
2626use uucore:: fs:: {
27- are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file,
28- path_ends_with_terminator,
27+ are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, canonicalize ,
28+ path_ends_with_terminator, MissingHandling , ResolveMode ,
2929} ;
3030#[ cfg( all( unix, not( any( target_os = "macos" , target_os = "redox" ) ) ) ) ]
3131use uucore:: fsxattr;
@@ -322,20 +322,6 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()>
322322 } ) ;
323323 }
324324
325- if ( source. eq ( target)
326- || are_hardlinks_to_same_file ( source, target)
327- || are_hardlinks_or_one_way_symlink_to_same_file ( source, target) )
328- && opts. backup == BackupMode :: NoBackup
329- {
330- if source. eq ( Path :: new ( "." ) ) || source. ends_with ( "/." ) || source. is_file ( ) {
331- return Err (
332- MvError :: SameFile ( source. quote ( ) . to_string ( ) , target. quote ( ) . to_string ( ) ) . into ( ) ,
333- ) ;
334- } else {
335- return Err ( MvError :: SelfSubdirectory ( source. display ( ) . to_string ( ) ) . into ( ) ) ;
336- }
337- }
338-
339325 let target_is_dir = target. is_dir ( ) ;
340326 let source_is_dir = source. is_dir ( ) ;
341327
@@ -347,6 +333,8 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()>
347333 return Err ( MvError :: FailedToAccessNotADirectory ( target. quote ( ) . to_string ( ) ) . into ( ) ) ;
348334 }
349335
336+ assert_not_same_file ( source, target, target_is_dir, opts) ?;
337+
350338 if target_is_dir {
351339 if opts. no_target_dir {
352340 if source. is_dir ( ) {
@@ -356,14 +344,6 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()>
356344 } else {
357345 Err ( MvError :: DirectoryToNonDirectory ( target. quote ( ) . to_string ( ) ) . into ( ) )
358346 }
359- // Check that source & target do not contain same subdir/dir when both exist
360- // mkdir dir1/dir2; mv dir1 dir1/dir2
361- } else if target. starts_with ( source) {
362- Err ( MvError :: SelfTargetSubdirectory (
363- source. display ( ) . to_string ( ) ,
364- target. display ( ) . to_string ( ) ,
365- )
366- . into ( ) )
367347 } else {
368348 move_files_into_dir ( & [ source. to_path_buf ( ) ] , target, opts)
369349 }
@@ -387,6 +367,88 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()>
387367 }
388368}
389369
370+ fn assert_not_same_file (
371+ source : & Path ,
372+ target : & Path ,
373+ target_is_dir : bool ,
374+ opts : & Options ,
375+ ) -> UResult < ( ) > {
376+ // we'll compare canonicalized_source and canonicalized_target for same file detection
377+ let canonicalized_source = match canonicalize (
378+ absolute ( source) ?,
379+ MissingHandling :: Normal ,
380+ ResolveMode :: Logical ,
381+ ) {
382+ Ok ( source) if source. exists ( ) => source,
383+ _ => absolute ( source) ?, // file or symlink target doesn't exist but its absolute path is still used for comparison
384+ } ;
385+
386+ // special case if the target exists, is a directory, and the `-T` flag wasn't used
387+ let target_is_dir = target_is_dir && !opts. no_target_dir ;
388+ let canonicalized_target = if target_is_dir {
389+ // `mv source_file target_dir` => target_dir/source_file
390+ // canonicalize the path that exists (target directory) and join the source file name
391+ canonicalize (
392+ absolute ( target) ?,
393+ MissingHandling :: Normal ,
394+ ResolveMode :: Logical ,
395+ ) ?
396+ . join ( source. file_name ( ) . unwrap_or_default ( ) )
397+ } else {
398+ // `mv source target_dir/target` => target_dir/target
399+ // we canonicalize target_dir and join /target
400+ match absolute ( target) ?. parent ( ) {
401+ Some ( parent) if parent. to_str ( ) != Some ( "" ) => {
402+ canonicalize ( parent, MissingHandling :: Normal , ResolveMode :: Logical ) ?
403+ . join ( target. file_name ( ) . unwrap_or_default ( ) )
404+ }
405+ // path.parent() returns Some("") or None if there's no parent
406+ _ => absolute ( target) ?, // absolute paths should always have a parent, but we'll fall back just in case
407+ }
408+ } ;
409+
410+ let same_file = ( canonicalized_source. eq ( & canonicalized_target)
411+ || are_hardlinks_to_same_file ( source, target)
412+ || are_hardlinks_or_one_way_symlink_to_same_file ( source, target) )
413+ && opts. backup == BackupMode :: NoBackup ;
414+
415+ // get the expected target path to show in errors
416+ // this is based on the argument and not canonicalized
417+ let target_display = match source. file_name ( ) {
418+ Some ( file_name) if target_is_dir => {
419+ // join target_dir/source_file in a platform-independent manner
420+ let mut path = target
421+ . display ( )
422+ . to_string ( )
423+ . trim_end_matches ( "/" )
424+ . to_owned ( ) ;
425+
426+ path. push ( '/' ) ;
427+ path. push_str ( & file_name. to_string_lossy ( ) ) ;
428+
429+ path. quote ( ) . to_string ( )
430+ }
431+ _ => target. quote ( ) . to_string ( ) ,
432+ } ;
433+
434+ if same_file
435+ && ( canonicalized_source. eq ( & canonicalized_target)
436+ || source. eq ( Path :: new ( "." ) )
437+ || source. ends_with ( "/." )
438+ || source. is_file ( ) )
439+ {
440+ return Err ( MvError :: SameFile ( source. quote ( ) . to_string ( ) , target_display) . into ( ) ) ;
441+ } else if ( same_file || canonicalized_target. starts_with ( canonicalized_source) )
442+ // don't error if we're moving a symlink of a directory into itself
443+ && !source. is_symlink ( )
444+ {
445+ return Err (
446+ MvError :: SelfTargetSubdirectory ( source. quote ( ) . to_string ( ) , target_display) . into ( ) ,
447+ ) ;
448+ }
449+ Ok ( ( ) )
450+ }
451+
390452fn handle_multiple_paths ( paths : & [ PathBuf ] , opts : & Options ) -> UResult < ( ) > {
391453 if opts. no_target_dir {
392454 return Err ( UUsageError :: new (
@@ -425,10 +487,6 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options)
425487 return Err ( MvError :: NotADirectory ( target_dir. quote ( ) . to_string ( ) ) . into ( ) ) ;
426488 }
427489
428- let canonicalized_target_dir = target_dir
429- . canonicalize ( )
430- . unwrap_or_else ( |_| target_dir. to_path_buf ( ) ) ;
431-
432490 let multi_progress = options. progress_bar . then ( MultiProgress :: new) ;
433491
434492 let count_progress = if let Some ( ref multi_progress) = multi_progress {
@@ -479,24 +537,9 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options)
479537
480538 // Check if we have mv dir1 dir2 dir2
481539 // And generate an error if this is the case
482- if let Ok ( canonicalized_source) = sourcepath. canonicalize ( ) {
483- if canonicalized_source == canonicalized_target_dir {
484- // User tried to move directory to itself, warning is shown
485- // and process of moving files is continued.
486- show ! ( USimpleError :: new(
487- 1 ,
488- format!(
489- "cannot move '{}' to a subdirectory of itself, '{}/{}'" ,
490- sourcepath. display( ) ,
491- uucore:: fs:: normalize_path( target_dir) . display( ) ,
492- canonicalized_target_dir. components( ) . last( ) . map_or_else(
493- || target_dir. display( ) . to_string( ) ,
494- |dir| { PathBuf :: from( dir. as_os_str( ) ) . display( ) . to_string( ) }
495- )
496- )
497- ) ) ;
498- continue ;
499- }
540+ if let Err ( e) = assert_not_same_file ( sourcepath, target_dir, true , options) {
541+ show ! ( e) ;
542+ continue ;
500543 }
501544
502545 match rename ( sourcepath, & targetpath, options, multi_progress. as_ref ( ) ) {
0 commit comments