Skip to content

Commit 5c8b54e

Browse files
committed
cp: gnu "same-file" test compatibility fix
1 parent 9a883fc commit 5c8b54e

File tree

3 files changed

+1043
-32
lines changed

3 files changed

+1043
-32
lines changed

src/uu/cp/src/cp.rs

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ use platform::copy_on_write;
2929
use uucore::display::Quotable;
3030
use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError};
3131
use uucore::fs::{
32-
are_hardlinks_to_same_file, canonicalize, is_symlink_loop, path_ends_with_terminator,
33-
paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode,
32+
are_hardlinks_to_same_file, canonicalize, get_filename, is_symlink_loop,
33+
path_ends_with_terminator, paths_refer_to_same_file, FileInformation, MissingHandling,
34+
ResolveMode,
3435
};
3536
use uucore::{backup_control, update_control};
3637
// These are exposed for projects (e.g. nushell) that want to create an `Options` value, which
@@ -1478,16 +1479,23 @@ pub(crate) fn copy_attributes(
14781479
fn symlink_file(
14791480
source: &Path,
14801481
dest: &Path,
1481-
context: &str,
14821482
symlinked_files: &mut HashSet<FileInformation>,
14831483
) -> CopyResult<()> {
14841484
#[cfg(not(windows))]
14851485
{
1486-
std::os::unix::fs::symlink(source, dest).context(context)?;
1486+
std::os::unix::fs::symlink(source, dest).context(format!(
1487+
"cannot create symlink {} to {}",
1488+
get_filename(dest).unwrap_or("invalid file name").quote(),
1489+
get_filename(source).unwrap_or("invalid file name").quote()
1490+
))?;
14871491
}
14881492
#[cfg(windows)]
14891493
{
1490-
std::os::windows::fs::symlink_file(source, dest).context(context)?;
1494+
std::os::windows::fs::symlink_file(source, dest).context(format!(
1495+
"cannot create symlink {} to {}",
1496+
get_filename(dest).unwrap_or("invalid file name").quote(),
1497+
get_filename(source).unwrap_or("invalid file name").quote()
1498+
))?;
14911499
}
14921500
if let Ok(file_info) = FileInformation::from_path(dest, false) {
14931501
symlinked_files.insert(file_info);
@@ -1499,10 +1507,11 @@ fn context_for(src: &Path, dest: &Path) -> String {
14991507
format!("{} -> {}", src.quote(), dest.quote())
15001508
}
15011509

1502-
/// Implements a simple backup copy for the destination file.
1510+
/// Implements a simple backup copy for the destination file .
1511+
/// if is_dest_symlink flag is set to true dest will be renamed to backup_path
15031512
/// TODO: for the backup, should this function be replaced by `copy_file(...)`?
1504-
fn backup_dest(dest: &Path, backup_path: &Path) -> CopyResult<PathBuf> {
1505-
if dest.is_symlink() {
1513+
fn backup_dest(dest: &Path, backup_path: &Path, is_dest_symlink: bool) -> CopyResult<PathBuf> {
1514+
if is_dest_symlink {
15061515
fs::rename(dest, backup_path)?;
15071516
} else {
15081517
fs::copy(dest, backup_path)?;
@@ -1523,11 +1532,38 @@ fn is_forbidden_to_copy_to_same_file(
15231532
) -> bool {
15241533
// TODO To match the behavior of GNU cp, we also need to check
15251534
// that the file is a regular file.
1535+
let source_is_symlink = source.is_symlink();
1536+
let dest_is_symlink = dest.is_symlink();
1537+
// only disable dereference if both source and dest is symlink and dereference flag is disabled
15261538
let dereference_to_compare =
1527-
options.dereference(source_in_command_line) || !source.is_symlink();
1528-
paths_refer_to_same_file(source, dest, dereference_to_compare)
1529-
&& !(options.force() && options.backup != BackupMode::NoBackup)
1530-
&& !(dest.is_symlink() && options.backup != BackupMode::NoBackup)
1539+
options.dereference(source_in_command_line) || (!source_is_symlink || !dest_is_symlink);
1540+
if !paths_refer_to_same_file(source, dest, dereference_to_compare) {
1541+
return false;
1542+
}
1543+
if options.backup != BackupMode::NoBackup {
1544+
if options.force() && !source_is_symlink {
1545+
return false;
1546+
}
1547+
if source_is_symlink && !options.dereference {
1548+
return false;
1549+
}
1550+
if dest_is_symlink {
1551+
return false;
1552+
}
1553+
if !dest_is_symlink && !source_is_symlink && dest != source {
1554+
return false;
1555+
}
1556+
}
1557+
if options.copy_mode == CopyMode::Link {
1558+
return false;
1559+
}
1560+
if options.copy_mode == CopyMode::SymLink && dest_is_symlink {
1561+
return false;
1562+
}
1563+
if dest_is_symlink && source_is_symlink && !options.dereference {
1564+
return false;
1565+
}
1566+
true
15311567
}
15321568

15331569
/// Back up, remove, or leave intact the destination file, depending on the options.
@@ -1536,6 +1572,7 @@ fn handle_existing_dest(
15361572
dest: &Path,
15371573
options: &Options,
15381574
source_in_command_line: bool,
1575+
copied_files: &mut HashMap<FileInformation, PathBuf>,
15391576
) -> CopyResult<()> {
15401577
// Disallow copying a file to itself, unless `--force` and
15411578
// `--backup` are both specified.
@@ -1547,6 +1584,7 @@ fn handle_existing_dest(
15471584
options.overwrite.verify(dest)?;
15481585
}
15491586

1587+
let mut is_dest_removed = false;
15501588
let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
15511589
if let Some(backup_path) = backup_path {
15521590
if paths_refer_to_same_file(source, &backup_path, true) {
@@ -1557,13 +1595,16 @@ fn handle_existing_dest(
15571595
)
15581596
.into());
15591597
} else {
1560-
backup_dest(dest, &backup_path)?;
1598+
is_dest_removed = dest.is_symlink();
1599+
backup_dest(dest, &backup_path, is_dest_removed)?;
15611600
}
15621601
}
15631602
match options.overwrite {
15641603
// FIXME: print that the file was removed if --verbose is enabled
15651604
OverwriteMode::Clobber(ClobberMode::Force) => {
1566-
if is_symlink_loop(dest) || fs::metadata(dest)?.permissions().readonly() {
1605+
if !is_dest_removed
1606+
&& (is_symlink_loop(dest) || fs::metadata(dest)?.permissions().readonly())
1607+
{
15671608
fs::remove_file(dest)?;
15681609
}
15691610
}
@@ -1584,7 +1625,19 @@ fn handle_existing_dest(
15841625
// `dest/src/f` and `dest/src/f` has the contents of
15851626
// `src/f`, we delete the existing file to allow the hard
15861627
// linking.
1587-
if options.preserve_hard_links() {
1628+
1629+
if options.preserve_hard_links()
1630+
// only try to remove dest file only if the current source
1631+
// is hardlink to a file that is already copied
1632+
&& copied_files.contains_key(
1633+
&FileInformation::from_path(
1634+
source,
1635+
options.dereference(source_in_command_line),
1636+
)
1637+
.context(format!("cannot stat {}", source.quote()))?,
1638+
)
1639+
&& !is_dest_removed
1640+
{
15881641
fs::remove_file(dest)?;
15891642
}
15901643
}
@@ -1710,7 +1763,7 @@ fn handle_copy_mode(
17101763
let backup_path =
17111764
backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
17121765
if let Some(backup_path) = backup_path {
1713-
backup_dest(dest, &backup_path)?;
1766+
backup_dest(dest, &backup_path, dest.is_symlink())?;
17141767
fs::remove_file(dest)?;
17151768
}
17161769
if options.overwrite == OverwriteMode::Clobber(ClobberMode::Force) {
@@ -1724,7 +1777,11 @@ fn handle_copy_mode(
17241777
} else {
17251778
fs::hard_link(source, dest)
17261779
}
1727-
.context(context)?;
1780+
.context(format!(
1781+
"cannot create hard link {} to {}",
1782+
get_filename(dest).unwrap_or("invalid file name").quote(),
1783+
get_filename(source).unwrap_or("invalid file name").quote()
1784+
))?;
17281785
}
17291786
CopyMode::Copy => {
17301787
copy_helper(
@@ -1741,7 +1798,7 @@ fn handle_copy_mode(
17411798
if dest.exists() && options.overwrite == OverwriteMode::Clobber(ClobberMode::Force) {
17421799
fs::remove_file(dest)?;
17431800
}
1744-
symlink_file(source, dest, context, symlinked_files)?;
1801+
symlink_file(source, dest, symlinked_files)?;
17451802
}
17461803
CopyMode::Update => {
17471804
if dest.exists() {
@@ -1870,8 +1927,10 @@ fn copy_file(
18701927
copied_files: &mut HashMap<FileInformation, PathBuf>,
18711928
source_in_command_line: bool,
18721929
) -> CopyResult<()> {
1930+
let source_is_symlink = source.is_symlink();
1931+
let dest_is_symlink = dest.is_symlink();
18731932
// Fail if dest is a dangling symlink or a symlink this program created previously
1874-
if dest.is_symlink() {
1933+
if dest_is_symlink {
18751934
if FileInformation::from_path(dest, false)
18761935
.map(|info| symlinked_files.contains(&info))
18771936
.unwrap_or(false)
@@ -1882,7 +1941,7 @@ fn copy_file(
18821941
dest.display()
18831942
)));
18841943
}
1885-
let copy_contents = options.dereference(source_in_command_line) || !source.is_symlink();
1944+
let copy_contents = options.dereference(source_in_command_line) || !source_is_symlink;
18861945
if copy_contents
18871946
&& !dest.exists()
18881947
&& !matches!(
@@ -1908,6 +1967,7 @@ fn copy_file(
19081967
}
19091968

19101969
if are_hardlinks_to_same_file(source, dest)
1970+
&& source != dest
19111971
&& matches!(
19121972
options.overwrite,
19131973
OverwriteMode::Clobber(ClobberMode::RemoveDestination)
@@ -1923,19 +1983,37 @@ fn copy_file(
19231983
OverwriteMode::Clobber(ClobberMode::RemoveDestination)
19241984
))
19251985
{
1926-
if are_hardlinks_to_same_file(source, dest)
1927-
&& !options.force()
1928-
&& options.backup == BackupMode::NoBackup
1929-
&& source != dest
1930-
|| (source == dest && options.copy_mode == CopyMode::Link)
1931-
{
1932-
return Ok(());
1986+
if paths_refer_to_same_file(source, dest, true) && options.copy_mode == CopyMode::Link {
1987+
if source_is_symlink {
1988+
if !dest_is_symlink {
1989+
return Ok(());
1990+
}
1991+
if !options.dereference {
1992+
return Ok(());
1993+
}
1994+
} else if options.backup != BackupMode::NoBackup && !dest_is_symlink {
1995+
if source == dest {
1996+
if !options.force() {
1997+
return Ok(());
1998+
}
1999+
} else {
2000+
return Ok(());
2001+
}
2002+
}
2003+
}
2004+
handle_existing_dest(source, dest, options, source_in_command_line, copied_files)?;
2005+
if are_hardlinks_to_same_file(source, dest) {
2006+
if options.copy_mode == CopyMode::Copy && options.backup != BackupMode::NoBackup {
2007+
return Ok(());
2008+
}
2009+
if options.copy_mode == CopyMode::Link && (!source_is_symlink || !dest_is_symlink) {
2010+
return Ok(());
2011+
}
19332012
}
1934-
handle_existing_dest(source, dest, options, source_in_command_line)?;
19352013
}
19362014

19372015
if options.attributes_only
1938-
&& source.is_symlink()
2016+
&& source_is_symlink
19392017
&& !matches!(
19402018
options.overwrite,
19412019
OverwriteMode::Clobber(ClobberMode::RemoveDestination)
@@ -1991,7 +2069,7 @@ fn copy_file(
19912069
)?;
19922070

19932071
// TODO: implement something similar to gnu's lchown
1994-
if !dest.is_symlink() {
2072+
if !dest_is_symlink {
19952073
// Here, to match GNU semantics, we quietly ignore an error
19962074
// if a user does not have the correct ownership to modify
19972075
// the permissions of a file.
@@ -2140,7 +2218,7 @@ fn copy_link(
21402218
if dest.is_symlink() || dest.is_file() {
21412219
fs::remove_file(dest)?;
21422220
}
2143-
symlink_file(&link, dest, &context_for(&link, dest), symlinked_files)
2221+
symlink_file(&link, dest, symlinked_files)
21442222
}
21452223

21462224
/// Generate an error message if `target` is not the correct `target_type`

src/uucore/src/lib/features/fs.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,15 @@ pub mod sane_blksize {
770770
}
771771
}
772772

773+
/// Returns the name of the file
774+
pub fn get_filename(file: &Path) -> Option<&str> {
775+
if let Some(filename) = file.file_name() {
776+
filename.to_str()
777+
} else {
778+
file.iter().next_back()?.to_str()
779+
}
780+
}
781+
773782
#[cfg(test)]
774783
mod tests {
775784
// Note this useful idiom: importing names from outer (for mod tests) scope.

0 commit comments

Comments
 (0)