#[cfg(windows)]
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::convert::identity;
use std::env;
use std::fs::{self, exists};
use std::io;
use std::path::{Path, PathBuf, StripPrefixError};
use indicatif::ProgressBar;
use uucore::display::Quotable;
use uucore::error::UIoError;
use uucore::fs::{
FileInformation, MissingHandling, ResolveMode, canonicalize, path_ends_with_terminator,
};
use uucore::show;
use uucore::show_error;
use uucore::translate;
use uucore::uio_error;
use walkdir::{DirEntry, WalkDir};
use crate::{
CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes, copy_file,
};
#[cfg(target_os = "windows")]
fn adjust_canonicalization(p: &Path) -> Cow<'_, Path> {
const VERBATIM_PREFIX: &str = r"\\?";
const DEVICE_NS_PREFIX: &str = r"\\.";
let has_prefix = p
.components()
.next()
.and_then(|comp| comp.as_os_str().to_str())
.is_some_and(|p_str| {
p_str.starts_with(VERBATIM_PREFIX) || p_str.starts_with(DEVICE_NS_PREFIX)
});
if has_prefix {
p.into()
} else {
Path::new(VERBATIM_PREFIX).join(p).into()
}
}
fn get_local_to_root_parent(
path: &Path,
root_parent: Option<&Path>,
) -> Result<PathBuf, StripPrefixError> {
match root_parent {
Some(parent) => {
#[cfg(windows)]
let (path, parent) = (
adjust_canonicalization(path),
adjust_canonicalization(parent),
);
let path = path.strip_prefix(parent)?;
Ok(path.to_path_buf())
}
None => Ok(path.to_path_buf()),
}
}
fn skip_last<T>(mut iter: impl Iterator<Item = T>) -> impl Iterator<Item = T> {
let last = iter.next();
iter.scan(last, |state, item| state.replace(item))
}
struct Context<'a> {
current_dir: PathBuf,
root_parent: Option<PathBuf>,
target: &'a Path,
target_is_file: bool,
root: &'a Path,
}
impl<'a> Context<'a> {
fn new(root: &'a Path, target: &'a Path) -> io::Result<Self> {
let current_dir = env::current_dir()?;
let root_path = current_dir.join(root);
let target_is_file = target.is_file();
let root_parent = if target.exists() && !root.to_str().unwrap().ends_with("/.") {
root_path.parent().map(|p| p.to_path_buf())
} else if root == Path::new(".") && target.is_dir() {
None
} else {
Some(root_path)
};
Ok(Self {
current_dir,
root_parent,
target,
target_is_file,
root,
})
}
}
struct Entry {
source_absolute: PathBuf,
source_relative: PathBuf,
local_to_target: PathBuf,
target_is_file: bool,
}
impl Entry {
fn new<A: AsRef<Path>>(
context: &Context,
source: A,
no_target_dir: bool,
) -> Result<Self, StripPrefixError> {
let source = source.as_ref();
let source_relative = source.to_path_buf();
let source_absolute = context.current_dir.join(&source_relative);
let mut descendant =
get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?;
if no_target_dir {
let source_is_dir = source.is_dir();
if path_ends_with_terminator(context.target)
&& source_is_dir
&& !exists(context.target).is_ok_and(identity)
{
if let Err(e) = fs::create_dir_all(context.target) {
eprintln!(
"{}",
translate!("cp-error-failed-to-create-directory", "error" => e)
);
}
} else if let Some(stripped) = context
.root
.components()
.next_back()
.and_then(|stripped| descendant.strip_prefix(stripped).ok())
{
descendant = stripped.to_path_buf();
}
} else if context.root == Path::new(".") && context.target.is_dir() {
if let Some(current_dir_name) = context.current_dir.file_name() {
if let Ok(stripped) = descendant.strip_prefix(current_dir_name) {
descendant = stripped.to_path_buf();
}
}
}
let local_to_target = context.target.join(descendant);
let target_is_file = context.target_is_file;
Ok(Self {
source_absolute,
source_relative,
local_to_target,
target_is_file,
})
}
}
#[allow(clippy::too_many_arguments)]
fn copy_direntry(
progress_bar: Option<&ProgressBar>,
entry: &Entry,
entry_is_symlink: bool,
entry_is_dir_no_follow: bool,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
preserve_hard_links: bool,
copied_destinations: &HashSet<PathBuf>,
copied_files: &mut HashMap<FileInformation, PathBuf>,
created_parent_dirs: &mut HashSet<PathBuf>,
) -> CopyResult<()> {
let source_is_symlink = entry_is_symlink;
let source_is_dir = if source_is_symlink && !options.dereference {
false
} else if source_is_symlink {
entry.source_absolute.is_dir()
} else {
entry_is_dir_no_follow
};
if source_is_dir && !entry.local_to_target.exists() {
return if entry.target_is_file {
Err(translate!("cp-error-cannot-overwrite-non-directory-with-directory").into())
} else {
build_dir(
&entry.local_to_target,
false,
options,
Some(&entry.source_absolute),
)?;
if options.verbose {
println!(
"{}",
context_for(&entry.source_relative, &entry.local_to_target)
);
}
Ok(())
};
}
if !source_is_dir {
if let Err(err) = copy_file(
progress_bar,
&entry.source_relative,
entry.local_to_target.as_path(),
options,
symlinked_files,
copied_destinations,
copied_files,
created_parent_dirs,
false,
) {
if preserve_hard_links {
if !source_is_symlink {
return Err(err);
}
} else {
match err {
CpError::IoErrContext(e, _) if e.kind() == io::ErrorKind::PermissionDenied => {
show!(uio_error!(
e,
"{}",
translate!(
"cp-error-cannot-open-for-reading",
"source" => entry.source_relative.quote()
),
));
}
e => return Err(e),
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn copy_directory(
progress_bar: Option<&ProgressBar>,
root: &Path,
target: &Path,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
copied_destinations: &HashSet<PathBuf>,
copied_files: &mut HashMap<FileInformation, PathBuf>,
created_parent_dirs: &mut HashSet<PathBuf>,
source_in_command_line: bool,
) -> CopyResult<()> {
if !options.dereference(source_in_command_line) && root.is_symlink() {
return copy_file(
progress_bar,
root,
target,
options,
symlinked_files,
copied_destinations,
copied_files,
created_parent_dirs,
source_in_command_line,
);
}
if !options.recursive {
return Err(translate!("cp-error-omitting-directory", "dir" => root.quote()).into());
}
if path_has_prefix(target, root)? {
let dest_name = root.file_name().unwrap_or(root.as_os_str());
return Err(translate!("cp-error-cannot-copy-directory-into-itself", "source" => root.quote(), "dest" => target.join(dest_name).quote())
.into());
}
let tmp = if options.parents {
if let Some(parent) = root.parent() {
let new_target = target.join(parent);
build_dir(&new_target, true, options, None)?;
if options.verbose {
for (x, y) in aligned_ancestors(root, &target.join(root)) {
println!("{} -> {}", x.display(), y.display());
}
}
new_target
} else {
target.to_path_buf()
}
} else {
target.to_path_buf()
};
let target = tmp.as_path();
let preserve_hard_links = options.preserve_hard_links();
let context = match Context::new(root, target) {
Ok(c) => c,
Err(e) => {
return Err(translate!("cp-error-failed-get-current-dir", "error" => e).into());
}
};
let mut last_iter: Option<DirEntry> = None;
let mut dirs_needing_permissions: Vec<(PathBuf, PathBuf)> = Vec::new();
for direntry_result in WalkDir::new(root)
.same_file_system(options.one_file_system)
.follow_links(options.dereference)
{
match direntry_result {
Ok(direntry) => {
let direntry_type = direntry.file_type();
let direntry_path = direntry.path();
let (entry_is_symlink, entry_is_dir_no_follow) =
match direntry_path.symlink_metadata() {
Ok(metadata) => {
let file_type = metadata.file_type();
(file_type.is_symlink(), file_type.is_dir())
}
Err(_) => (direntry_type.is_symlink(), direntry_type.is_dir()),
};
let entry = Entry::new(&context, direntry_path, options.no_target_dir)?;
copy_direntry(
progress_bar,
&entry,
entry_is_symlink,
entry_is_dir_no_follow,
options,
symlinked_files,
preserve_hard_links,
copied_destinations,
copied_files,
created_parent_dirs,
)?;
let is_dir_for_permissions =
entry_is_dir_no_follow || (options.dereference && direntry_path.is_dir());
if is_dir_for_permissions {
dirs_needing_permissions
.push((entry.source_absolute.clone(), entry.local_to_target.clone()));
let went_up = if let Some(last_iter) = &last_iter {
last_iter.path().strip_prefix(direntry_path).is_ok()
} else {
false
};
if went_up {
let last_iter = last_iter.as_ref().unwrap();
let diff = last_iter.path().strip_prefix(direntry_path).unwrap();
for p in skip_last(diff.ancestors()) {
let src = direntry_path.join(p);
let entry = Entry::new(&context, &src, options.no_target_dir)?;
copy_attributes(
&entry.source_absolute,
&entry.local_to_target,
&options.attributes,
)?;
}
}
last_iter = Some(direntry);
}
}
Err(e) => show_error!("{e}"),
}
}
for (source_path, dest_path) in dirs_needing_permissions {
copy_attributes(&source_path, &dest_path, &options.attributes)?;
}
if options.parents {
let dest = target.join(root.file_name().unwrap());
for (x, y) in aligned_ancestors(root, dest.as_path()) {
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
copy_attributes(&src, y, &options.attributes)?;
}
}
}
Ok(())
}
pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result<bool> {
let pathbuf1 = canonicalize(p1, MissingHandling::Normal, ResolveMode::Logical)?;
let pathbuf2 = canonicalize(p2, MissingHandling::Normal, ResolveMode::Logical)?;
Ok(pathbuf1.starts_with(pathbuf2))
}
#[allow(unused_variables)]
fn build_dir(
path: &PathBuf,
recursive: bool,
options: &Options,
copy_attributes_from: Option<&Path>,
) -> CopyResult<()> {
let mut builder = fs::DirBuilder::new();
builder.recursive(recursive);
#[cfg(unix)]
{
use crate::Preserve;
use std::os::unix::fs::PermissionsExt;
#[allow(clippy::unnecessary_cast)]
let mut excluded_perms = if matches!(options.attributes.ownership, Preserve::Yes { .. }) {
libc::S_IRWXG | libc::S_IRWXO } else if matches!(options.attributes.mode, Preserve::Yes { .. }) {
libc::S_IWGRP | libc::S_IWOTH } else {
0
} as u32;
let umask = if let (Some(from), Preserve::Yes { .. }) =
(copy_attributes_from, options.attributes.mode)
{
!fs::symlink_metadata(from)?.permissions().mode()
} else {
uucore::mode::get_umask()
};
excluded_perms |= umask;
let mode = !excluded_perms & 0o777; std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode);
}
builder.create(path)?;
Ok(())
}