Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
5a94557
fix-issue-5031
tommady Jul 10, 2023
ea8794c
Merge branch 'main' into fix-issue-5031
tommady Jul 10, 2023
31c1ebf
fix spelling
tommady Jul 10, 2023
1dfa23f
add explanation
tommady Jul 11, 2023
cde4af8
Merge branch 'main' into fix-issue-5031
tommady Jul 12, 2023
efbdf51
adding testcase for 5031
tommady Jul 13, 2023
a3803af
adding new testcase for 5031
tommady Jul 13, 2023
37e3a7c
Fix a typo
sylvestre Jul 13, 2023
0cd08e7
add testcase 3
tommady Jul 13, 2023
fa4ea63
Merge branch 'main' into fix-issue-5031
tommady Jul 13, 2023
930137c
remove unused code
tommady Jul 13, 2023
86a96fa
Merge branch 'main' into fix-issue-5031
tommady Jul 17, 2023
8820a04
by pass case 3 unit test on android
tommady Jul 19, 2023
bc9a054
Merge branch 'main' into fix-issue-5031
tommady Jul 19, 2023
b94be4c
by pass case 1 and 2 unit tests on android
tommady Jul 19, 2023
5ede19a
Merge branch 'main' into fix-issue-5031
tommady Jul 19, 2023
94fb212
add 5031 ut case 4 and fix it
tommady Jul 25, 2023
f49b86b
add 5031 ut case 5
tommady Jul 25, 2023
28278e2
fix linter issues
tommady Jul 25, 2023
bdaa394
Merge branch 'main' into fix-issue-5031
tommady Jul 25, 2023
395cee4
fix unit test failed
tommady Jul 25, 2023
48e7f2e
Merge branch 'main' into fix-issue-5031
tommady Jul 27, 2023
87bae22
align preserve links unit tests with gnu cp tests
tommady Aug 2, 2023
bf87a44
correct preserve links unit test case 5
tommady Aug 2, 2023
340df6b
using single copied FileInformation map to do the job
tommady Aug 3, 2023
35000cf
Merge branch 'fix-issue-5031' of github.com:tommady/coreutils into fi…
tommady Aug 3, 2023
cb65994
Merge branch 'main' into fix-issue-5031
tommady Aug 3, 2023
5bf83d0
remove unused clippy too many arg allowance
tommady Aug 3, 2023
26b5727
adding some comments to explain the copied_file hashmap and the hard_…
tommady Aug 3, 2023
36487a9
fix ERROR: Unknown word (hashmap's)
tommady Aug 3, 2023
d77e2a5
fix ERROR: Unknown word (files's)
tommady Aug 3, 2023
49edf6b
Merge branch 'main' into fix-issue-5031
tommady Aug 10, 2023
ba69841
let the unit tests run on android and freebsd
tommady Aug 11, 2023
57718f8
Merge branch 'main' into fix-issue-5031
tommady Aug 11, 2023
2b7e3c4
let the unit tests run on unix platform while the inode assertion
tommady Aug 11, 2023
844d99b
Merge branch 'main' into fix-issue-5031
tommady Aug 11, 2023
3554e62
let MetadataExt can be used while testing on freebsd platform
tommady Aug 12, 2023
2743c11
Merge branch 'main' into fix-issue-5031
tommady Aug 12, 2023
c592a0a
address comments
tommady Aug 13, 2023
f65fd74
Merge branch 'main' into fix-issue-5031
tommady Aug 13, 2023
8e9c2a3
Merge branch 'main' into fix-issue-5031
tommady Aug 14, 2023
be274d3
Merge branch 'main' into fix-issue-5031
tommady Aug 15, 2023
926db5e
Merge branch 'main' into fix-issue-5031
tommady Aug 20, 2023
5e59458
address comment move the copied_files hashmap insertion from each cop…
tommady Aug 22, 2023
abb2c0f
restore the dest variable back to it's original place of definition
tommady Aug 22, 2023
39ad651
fix clippy linter issue
tommady Aug 22, 2023
4eb2a27
Merge branch 'main' into fix-issue-5031
tommady Aug 22, 2023
04f4abc
address comment, merge preserve_hardlinks function
tommady Aug 27, 2023
4af43c4
Merge branch 'fix-issue-5031' of github.com:tommady/coreutils into fi…
tommady Aug 27, 2023
1377e6f
Merge branch 'main' into fix-issue-5031
tommady Aug 27, 2023
8dc3deb
fix clippy linter issue
tommady Aug 27, 2023
8adcafb
Merge branch 'fix-issue-5031' of github.com:tommady/coreutils into fi…
tommady Aug 27, 2023
0abb096
address comments
tommady Aug 29, 2023
fd4b7cf
address comments
tommady Aug 30, 2023
f81145e
address comments
tommady Aug 30, 2023
d7d58b0
Merge branch 'main' into fix-issue-5031
tommady Sep 6, 2023
5827f91
Merge branch 'main' into fix-issue-5031
tommady Sep 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 28 additions & 29 deletions src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! See the [`copy_directory`] function for more information.
#[cfg(windows)]
use std::borrow::Cow;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use std::io;
Expand All @@ -24,8 +24,8 @@ use uucore::uio_error;
use walkdir::{DirEntry, WalkDir};

use crate::{
aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, preserve_hardlinks,
CopyResult, Error, Options,
aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, CopyResult, Error,
Options,
};

/// Ensure a Windows path starts with a `\\?`.
Expand Down Expand Up @@ -200,7 +200,7 @@ fn copy_direntry(
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
preserve_hard_links: bool,
hard_links: &mut Vec<(String, u64)>,
copied_files: &mut HashMap<FileInformation, PathBuf>,
) -> CopyResult<()> {
let Entry {
source_absolute,
Expand Down Expand Up @@ -240,30 +240,27 @@ fn copy_direntry(
// If the source is not a directory, then we need to copy the file.
if !source_absolute.is_dir() {
if preserve_hard_links {
let dest = local_to_target.as_path().to_path_buf();
let found_hard_link = preserve_hardlinks(hard_links, &source_absolute, &dest)?;
if !found_hard_link {
match copy_file(
progress_bar,
&source_absolute,
local_to_target.as_path(),
options,
symlinked_files,
false,
) {
Ok(_) => Ok(()),
Err(err) => {
if source_absolute.is_symlink() {
// silent the error with a symlink
// In case we do --archive, we might copy the symlink
// before the file itself
Ok(())
} else {
Err(err)
}
match copy_file(
progress_bar,
&source_absolute,
local_to_target.as_path(),
options,
symlinked_files,
copied_files,
false,
) {
Ok(_) => Ok(()),
Err(err) => {
if source_absolute.is_symlink() {
// silent the error with a symlink
// In case we do --archive, we might copy the symlink
// before the file itself
Ok(())
} else {
Err(err)
}
}?;
}
}
}?;
} else {
// At this point, `path` is just a plain old file.
// Terminate this function immediately if there is any
Expand All @@ -277,6 +274,7 @@ fn copy_direntry(
local_to_target.as_path(),
options,
symlinked_files,
copied_files,
false,
) {
Ok(_) => {}
Expand Down Expand Up @@ -310,6 +308,7 @@ pub(crate) fn copy_directory(
target: &Path,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
copied_files: &mut HashMap<FileInformation, PathBuf>,
source_in_command_line: bool,
) -> CopyResult<()> {
if !options.recursive {
Expand All @@ -324,6 +323,7 @@ pub(crate) fn copy_directory(
target,
options,
symlinked_files,
copied_files,
source_in_command_line,
);
}
Expand Down Expand Up @@ -372,7 +372,6 @@ pub(crate) fn copy_directory(
};
let target = tmp.as_path();

let mut hard_links: Vec<(String, u64)> = vec![];
let preserve_hard_links = options.preserve_hard_links();

// Collect some paths here that are invariant during the traversal
Expand All @@ -397,7 +396,7 @@ pub(crate) fn copy_directory(
options,
symlinked_files,
preserve_hard_links,
&mut hard_links,
copied_files,
)?;
}
// Print an error message, but continue traversing the directory.
Expand Down
158 changes: 70 additions & 88 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use quick_error::quick_error;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::env;
#[cfg(not(windows))]
use std::ffi::CString;
Expand Down Expand Up @@ -1106,67 +1106,6 @@ fn parse_path_args(
Ok((paths, target))
}

/// Get the inode information for a file.
fn get_inode(file_info: &FileInformation) -> u64 {
#[cfg(unix)]
let result = file_info.inode();
#[cfg(windows)]
let result = file_info.file_index();
result
}

#[cfg(target_os = "redox")]
fn preserve_hardlinks(
hard_links: &mut Vec<(String, u64)>,
source: &std::path::Path,
dest: &std::path::Path,
found_hard_link: &mut bool,
) -> CopyResult<()> {
// Redox does not currently support hard links
Ok(())
}

/// Hard link a pair of files if needed _and_ record if this pair is a new hard link.
#[cfg(not(target_os = "redox"))]
fn preserve_hardlinks(
hard_links: &mut Vec<(String, u64)>,
source: &std::path::Path,
dest: &std::path::Path,
) -> CopyResult<bool> {
let info = FileInformation::from_path(source, false)
.context(format!("cannot stat {}", source.quote()))?;
let inode = get_inode(&info);
let nlinks = info.number_of_links();
let mut found_hard_link = false;
#[allow(clippy::explicit_iter_loop)]
for (link, link_inode) in hard_links.iter() {
if *link_inode == inode {
// Consider the following files:
//
// * `src/f` - a regular file
// * `src/link` - a hard link to `src/f`
// * `dest/src/f` - a different regular file
//
// In this scenario, if we do `cp -a src/ dest/`, it is
// possible that the order of traversal causes `src/link`
// to get copied first (to `dest/src/link`). In that case,
// in order to make sure `dest/src/link` is a hard link to
// `dest/src/f` and `dest/src/f` has the contents of
// `src/f`, we delete the existing file to allow the hard
// linking.
if file_or_link_exists(dest) && file_or_link_exists(Path::new(link)) {
std::fs::remove_file(dest)?;
}
std::fs::hard_link(link, dest).unwrap();
found_hard_link = true;
}
}
if !found_hard_link && nlinks > 1 {
hard_links.push((dest.to_str().unwrap().to_string(), inode));
}
Ok(found_hard_link)
}

/// When handling errors, we don't always want to show them to the user. This function handles that.
fn show_error_if_needed(error: &Error) {
match error {
Expand Down Expand Up @@ -1195,14 +1134,19 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult
let target_type = TargetType::determine(sources, target);
verify_target_type(target, &target_type)?;

let preserve_hard_links = options.preserve_hard_links();

let mut hard_links: Vec<(String, u64)> = vec![];

let mut non_fatal_errors = false;
let mut seen_sources = HashSet::with_capacity(sources.len());
let mut symlinked_files = HashSet::new();

// to remember the copied files for further usage.
// the FileInformation implemented the Hash trait by using
// 1. inode number
// 2. device number
// the combination of a file's inode number and device number is unique throughout all the file systems.
//
// key is the source file's information and the value is the destination filepath.
let mut copied_files: HashMap<FileInformation, PathBuf> = HashMap::with_capacity(sources.len());

let progress_bar = if options.progress_bar {
let pb = ProgressBar::new(disk_usage(sources, options.recursive)?)
.with_style(
Expand All @@ -1222,28 +1166,19 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult
if seen_sources.contains(source) {
// FIXME: compare sources by the actual file they point to, not their path. (e.g. dir/file == dir/../dir/file in most cases)
show_warning!("source {} specified more than once", source.quote());
} else {
let found_hard_link = if preserve_hard_links && !source.is_dir() {
let dest = construct_dest_path(source, target, &target_type, options)?;
preserve_hardlinks(&mut hard_links, source, &dest)?
} else {
false
};
if !found_hard_link {
if let Err(error) = copy_source(
&progress_bar,
source,
target,
&target_type,
options,
&mut symlinked_files,
) {
show_error_if_needed(&error);
non_fatal_errors = true;
}
}
seen_sources.insert(source);
} else if let Err(error) = copy_source(
&progress_bar,
source,
target,
&target_type,
options,
&mut symlinked_files,
&mut copied_files,
) {
show_error_if_needed(&error);
non_fatal_errors = true;
}
seen_sources.insert(source);
}

if let Some(pb) = progress_bar {
Expand Down Expand Up @@ -1295,11 +1230,20 @@ fn copy_source(
target_type: &TargetType,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
copied_files: &mut HashMap<FileInformation, PathBuf>,
) -> CopyResult<()> {
let source_path = Path::new(&source);
if source_path.is_dir() {
// Copy as directory
copy_directory(progress_bar, source, target, options, symlinked_files, true)
copy_directory(
progress_bar,
source,
target,
options,
symlinked_files,
copied_files,
true,
)
} else {
// Copy as file
let dest = construct_dest_path(source_path, target, target_type, options)?;
Expand All @@ -1309,6 +1253,7 @@ fn copy_source(
dest.as_path(),
options,
symlinked_files,
copied_files,
true,
);
if options.parents {
Expand Down Expand Up @@ -1570,6 +1515,24 @@ fn handle_existing_dest(
OverwriteMode::Clobber(ClobberMode::RemoveDestination) => {
fs::remove_file(dest)?;
}
OverwriteMode::Clobber(ClobberMode::Standard) => {
// Consider the following files:
//
// * `src/f` - a regular file
// * `src/link` - a hard link to `src/f`
// * `dest/src/f` - a different regular file
//
// In this scenario, if we do `cp -a src/ dest/`, it is
// possible that the order of traversal causes `src/link`
// to get copied first (to `dest/src/link`). In that case,
// in order to make sure `dest/src/link` is a hard link to
// `dest/src/f` and `dest/src/f` has the contents of
// `src/f`, we delete the existing file to allow the hard
// linking.
if options.preserve_hard_links() {
fs::remove_file(dest)?;
}
}
_ => (),
};

Expand Down Expand Up @@ -1643,6 +1606,7 @@ fn copy_file(
dest: &Path,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
copied_files: &mut HashMap<FileInformation, PathBuf>,
source_in_command_line: bool,
) -> CopyResult<()> {
if (options.update == UpdateMode::ReplaceIfOlder || options.update == UpdateMode::ReplaceNone)
Expand Down Expand Up @@ -1686,6 +1650,19 @@ fn copy_file(
handle_existing_dest(source, dest, options, source_in_command_line)?;
}

if options.preserve_hard_links() {
// if we encounter a matching device/inode pair in the source tree
// we can arrange to create a hard link between the corresponding names
// in the destination tree.
if let Some(new_source) = copied_files.get(
&FileInformation::from_path(source, options.dereference(source_in_command_line))
.context(format!("cannot stat {}", source.quote()))?,
) {
std::fs::hard_link(new_source, dest)?;
return Ok(());
};
}

if options.verbose {
if let Some(pb) = progress_bar {
// Suspend (hide) the progress bar so the println won't overlap with the progress bar.
Expand Down Expand Up @@ -1873,6 +1850,11 @@ fn copy_file(

copy_attributes(source, dest, &options.attributes)?;

copied_files.insert(
FileInformation::from_path(source, options.dereference(source_in_command_line))?,
dest.to_path_buf(),
);

if let Some(progress_bar) = progress_bar {
progress_bar.inc(fs::metadata(source)?.len());
}
Expand Down
Loading