Skip to content

Commit f47875c

Browse files
matrixheadsylvestre
authored andcommitted
cp: gnu "same-file" test compatibility fix
1 parent 151b2cc commit f47875c

File tree

3 files changed

+1058
-32
lines changed

3 files changed

+1058
-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
@@ -1468,16 +1469,23 @@ pub(crate) fn copy_attributes(
14681469
fn symlink_file(
14691470
source: &Path,
14701471
dest: &Path,
1471-
context: &str,
14721472
symlinked_files: &mut HashSet<FileInformation>,
14731473
) -> CopyResult<()> {
14741474
#[cfg(not(windows))]
14751475
{
1476-
std::os::unix::fs::symlink(source, dest).context(context)?;
1476+
std::os::unix::fs::symlink(source, dest).context(format!(
1477+
"cannot create symlink {} to {}",
1478+
get_filename(dest).unwrap_or("invalid file name").quote(),
1479+
get_filename(source).unwrap_or("invalid file name").quote()
1480+
))?;
14771481
}
14781482
#[cfg(windows)]
14791483
{
1480-
std::os::windows::fs::symlink_file(source, dest).context(context)?;
1484+
std::os::windows::fs::symlink_file(source, dest).context(format!(
1485+
"cannot create symlink {} to {}",
1486+
get_filename(dest).unwrap_or("invalid file name").quote(),
1487+
get_filename(source).unwrap_or("invalid file name").quote()
1488+
))?;
14811489
}
14821490
if let Ok(file_info) = FileInformation::from_path(dest, false) {
14831491
symlinked_files.insert(file_info);
@@ -1489,10 +1497,11 @@ fn context_for(src: &Path, dest: &Path) -> String {
14891497
format!("{} -> {}", src.quote(), dest.quote())
14901498
}
14911499

1492-
/// Implements a simple backup copy for the destination file.
1500+
/// Implements a simple backup copy for the destination file .
1501+
/// if is_dest_symlink flag is set to true dest will be renamed to backup_path
14931502
/// TODO: for the backup, should this function be replaced by `copy_file(...)`?
1494-
fn backup_dest(dest: &Path, backup_path: &Path) -> CopyResult<PathBuf> {
1495-
if dest.is_symlink() {
1503+
fn backup_dest(dest: &Path, backup_path: &Path, is_dest_symlink: bool) -> CopyResult<PathBuf> {
1504+
if is_dest_symlink {
14961505
fs::rename(dest, backup_path)?;
14971506
} else {
14981507
fs::copy(dest, backup_path)?;
@@ -1513,11 +1522,38 @@ fn is_forbidden_to_copy_to_same_file(
15131522
) -> bool {
15141523
// TODO To match the behavior of GNU cp, we also need to check
15151524
// that the file is a regular file.
1525+
let source_is_symlink = source.is_symlink();
1526+
let dest_is_symlink = dest.is_symlink();
1527+
// only disable dereference if both source and dest is symlink and dereference flag is disabled
15161528
let dereference_to_compare =
1517-
options.dereference(source_in_command_line) || !source.is_symlink();
1518-
paths_refer_to_same_file(source, dest, dereference_to_compare)
1519-
&& !(options.force() && options.backup != BackupMode::NoBackup)
1520-
&& !(dest.is_symlink() && options.backup != BackupMode::NoBackup)
1529+
options.dereference(source_in_command_line) || (!source_is_symlink || !dest_is_symlink);
1530+
if !paths_refer_to_same_file(source, dest, dereference_to_compare) {
1531+
return false;
1532+
}
1533+
if options.backup != BackupMode::NoBackup {
1534+
if options.force() && !source_is_symlink {
1535+
return false;
1536+
}
1537+
if source_is_symlink && !options.dereference {
1538+
return false;
1539+
}
1540+
if dest_is_symlink {
1541+
return false;
1542+
}
1543+
if !dest_is_symlink && !source_is_symlink && dest != source {
1544+
return false;
1545+
}
1546+
}
1547+
if options.copy_mode == CopyMode::Link {
1548+
return false;
1549+
}
1550+
if options.copy_mode == CopyMode::SymLink && dest_is_symlink {
1551+
return false;
1552+
}
1553+
if dest_is_symlink && source_is_symlink && !options.dereference {
1554+
return false;
1555+
}
1556+
true
15211557
}
15221558

15231559
/// Back up, remove, or leave intact the destination file, depending on the options.
@@ -1526,6 +1562,7 @@ fn handle_existing_dest(
15261562
dest: &Path,
15271563
options: &Options,
15281564
source_in_command_line: bool,
1565+
copied_files: &mut HashMap<FileInformation, PathBuf>,
15291566
) -> CopyResult<()> {
15301567
// Disallow copying a file to itself, unless `--force` and
15311568
// `--backup` are both specified.
@@ -1537,6 +1574,7 @@ fn handle_existing_dest(
15371574
options.overwrite.verify(dest)?;
15381575
}
15391576

1577+
let mut is_dest_removed = false;
15401578
let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
15411579
if let Some(backup_path) = backup_path {
15421580
if paths_refer_to_same_file(source, &backup_path, true) {
@@ -1547,13 +1585,16 @@ fn handle_existing_dest(
15471585
)
15481586
.into());
15491587
} else {
1550-
backup_dest(dest, &backup_path)?;
1588+
is_dest_removed = dest.is_symlink();
1589+
backup_dest(dest, &backup_path, is_dest_removed)?;
15511590
}
15521591
}
15531592
match options.overwrite {
15541593
// FIXME: print that the file was removed if --verbose is enabled
15551594
OverwriteMode::Clobber(ClobberMode::Force) => {
1556-
if is_symlink_loop(dest) || fs::metadata(dest)?.permissions().readonly() {
1595+
if !is_dest_removed
1596+
&& (is_symlink_loop(dest) || fs::metadata(dest)?.permissions().readonly())
1597+
{
15571598
fs::remove_file(dest)?;
15581599
}
15591600
}
@@ -1574,7 +1615,19 @@ fn handle_existing_dest(
15741615
// `dest/src/f` and `dest/src/f` has the contents of
15751616
// `src/f`, we delete the existing file to allow the hard
15761617
// linking.
1577-
if options.preserve_hard_links() {
1618+
1619+
if options.preserve_hard_links()
1620+
// only try to remove dest file only if the current source
1621+
// is hardlink to a file that is already copied
1622+
&& copied_files.contains_key(
1623+
&FileInformation::from_path(
1624+
source,
1625+
options.dereference(source_in_command_line),
1626+
)
1627+
.context(format!("cannot stat {}", source.quote()))?,
1628+
)
1629+
&& !is_dest_removed
1630+
{
15781631
fs::remove_file(dest)?;
15791632
}
15801633
}
@@ -1700,7 +1753,7 @@ fn handle_copy_mode(
17001753
let backup_path =
17011754
backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
17021755
if let Some(backup_path) = backup_path {
1703-
backup_dest(dest, &backup_path)?;
1756+
backup_dest(dest, &backup_path, dest.is_symlink())?;
17041757
fs::remove_file(dest)?;
17051758
}
17061759
if options.overwrite == OverwriteMode::Clobber(ClobberMode::Force) {
@@ -1714,7 +1767,11 @@ fn handle_copy_mode(
17141767
} else {
17151768
fs::hard_link(source, dest)
17161769
}
1717-
.context(context)?;
1770+
.context(format!(
1771+
"cannot create hard link {} to {}",
1772+
get_filename(dest).unwrap_or("invalid file name").quote(),
1773+
get_filename(source).unwrap_or("invalid file name").quote()
1774+
))?;
17181775
}
17191776
CopyMode::Copy => {
17201777
copy_helper(
@@ -1731,7 +1788,7 @@ fn handle_copy_mode(
17311788
if dest.exists() && options.overwrite == OverwriteMode::Clobber(ClobberMode::Force) {
17321789
fs::remove_file(dest)?;
17331790
}
1734-
symlink_file(source, dest, context, symlinked_files)?;
1791+
symlink_file(source, dest, symlinked_files)?;
17351792
}
17361793
CopyMode::Update => {
17371794
if dest.exists() {
@@ -1860,8 +1917,10 @@ fn copy_file(
18601917
copied_files: &mut HashMap<FileInformation, PathBuf>,
18611918
source_in_command_line: bool,
18621919
) -> CopyResult<()> {
1920+
let source_is_symlink = source.is_symlink();
1921+
let dest_is_symlink = dest.is_symlink();
18631922
// Fail if dest is a dangling symlink or a symlink this program created previously
1864-
if dest.is_symlink() {
1923+
if dest_is_symlink {
18651924
if FileInformation::from_path(dest, false)
18661925
.map(|info| symlinked_files.contains(&info))
18671926
.unwrap_or(false)
@@ -1872,7 +1931,7 @@ fn copy_file(
18721931
dest.display()
18731932
)));
18741933
}
1875-
let copy_contents = options.dereference(source_in_command_line) || !source.is_symlink();
1934+
let copy_contents = options.dereference(source_in_command_line) || !source_is_symlink;
18761935
if copy_contents
18771936
&& !dest.exists()
18781937
&& !matches!(
@@ -1898,6 +1957,7 @@ fn copy_file(
18981957
}
18991958

19001959
if are_hardlinks_to_same_file(source, dest)
1960+
&& source != dest
19011961
&& matches!(
19021962
options.overwrite,
19031963
OverwriteMode::Clobber(ClobberMode::RemoveDestination)
@@ -1913,19 +1973,37 @@ fn copy_file(
19131973
OverwriteMode::Clobber(ClobberMode::RemoveDestination)
19141974
))
19151975
{
1916-
if are_hardlinks_to_same_file(source, dest)
1917-
&& !options.force()
1918-
&& options.backup == BackupMode::NoBackup
1919-
&& source != dest
1920-
|| (source == dest && options.copy_mode == CopyMode::Link)
1921-
{
1922-
return Ok(());
1976+
if paths_refer_to_same_file(source, dest, true) && options.copy_mode == CopyMode::Link {
1977+
if source_is_symlink {
1978+
if !dest_is_symlink {
1979+
return Ok(());
1980+
}
1981+
if !options.dereference {
1982+
return Ok(());
1983+
}
1984+
} else if options.backup != BackupMode::NoBackup && !dest_is_symlink {
1985+
if source == dest {
1986+
if !options.force() {
1987+
return Ok(());
1988+
}
1989+
} else {
1990+
return Ok(());
1991+
}
1992+
}
1993+
}
1994+
handle_existing_dest(source, dest, options, source_in_command_line, copied_files)?;
1995+
if are_hardlinks_to_same_file(source, dest) {
1996+
if options.copy_mode == CopyMode::Copy && options.backup != BackupMode::NoBackup {
1997+
return Ok(());
1998+
}
1999+
if options.copy_mode == CopyMode::Link && (!source_is_symlink || !dest_is_symlink) {
2000+
return Ok(());
2001+
}
19232002
}
1924-
handle_existing_dest(source, dest, options, source_in_command_line)?;
19252003
}
19262004

19272005
if options.attributes_only
1928-
&& source.is_symlink()
2006+
&& source_is_symlink
19292007
&& !matches!(
19302008
options.overwrite,
19312009
OverwriteMode::Clobber(ClobberMode::RemoveDestination)
@@ -1981,7 +2059,7 @@ fn copy_file(
19812059
)?;
19822060

19832061
// TODO: implement something similar to gnu's lchown
1984-
if !dest.is_symlink() {
2062+
if !dest_is_symlink {
19852063
// Here, to match GNU semantics, we quietly ignore an error
19862064
// if a user does not have the correct ownership to modify
19872065
// the permissions of a file.
@@ -2130,7 +2208,7 @@ fn copy_link(
21302208
if dest.is_symlink() || dest.is_file() {
21312209
fs::remove_file(dest)?;
21322210
}
2133-
symlink_file(&link, dest, &context_for(&link, dest), symlinked_files)
2211+
symlink_file(&link, dest, symlinked_files)
21342212
}
21352213

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

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

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

773+
/// Extracts the filename component from the given `file` path and returns it as an `Option<&str>`.
774+
///
775+
/// If the `file` path contains a filename, this function returns `Some(filename)` where `filename` is
776+
/// the extracted filename as a string slice (`&str`). If the `file` path does not have a filename
777+
/// component or if the filename is not valid UTF-8, it returns `None`.
778+
///
779+
/// # Arguments
780+
///
781+
/// * `file`: A reference to a `Path` representing the file path from which to extract the filename.
782+
///
783+
/// # Returns
784+
///
785+
/// * `Some(filename)`: If a valid filename exists in the `file` path, where `filename` is the
786+
/// extracted filename as a string slice (`&str`).
787+
/// * `None`: If the `file` path does not contain a valid filename or if the filename is not valid UTF-8.
788+
pub fn get_filename(file: &Path) -> Option<&str> {
789+
file.file_name().and_then(|filename| filename.to_str())
790+
}
791+
773792
#[cfg(test)]
774793
mod tests {
775794
// Note this useful idiom: importing names from outer (for mod tests) scope.
@@ -1006,4 +1025,9 @@ mod tests {
10061025
assert_eq!(0x2000_0000, sane_blksize::sane_blksize(0x2000_0000));
10071026
assert_eq!(512, sane_blksize::sane_blksize(0x2000_0001));
10081027
}
1028+
#[test]
1029+
fn test_get_file_name() {
1030+
let file_path = PathBuf::from("~/foo.txt");
1031+
assert!(matches!(get_filename(&file_path), Some("foo.txt")));
1032+
}
10091033
}

0 commit comments

Comments
 (0)