#[cfg(windows)]
use std::borrow::Cow;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf, StripPrefixError};
use indicatif::ProgressBar;
use uucore::display::Quotable;
use uucore::error::UIoError;
use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode};
use uucore::show;
use uucore::show_error;
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,
};
#[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())
.map(|p_str| p_str.starts_with(VERBATIM_PREFIX) || p_str.starts_with(DEVICE_NS_PREFIX))
.unwrap_or_default();
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()),
}
}
struct Context<'a> {
current_dir: PathBuf,
root_parent: Option<PathBuf>,
target: &'a Path,
}
impl<'a> Context<'a> {
fn new(root: &'a Path, target: &'a Path) -> std::io::Result<Self> {
let current_dir = env::current_dir()?;
let root_path = current_dir.join(root);
let root_parent = if target.exists() && !root.to_str().unwrap().ends_with("/.") {
root_path.parent().map(|p| p.to_path_buf())
} else {
Some(root_path)
};
Ok(Self {
current_dir,
root_parent,
target,
})
}
}
struct Entry {
source_absolute: PathBuf,
source_relative: PathBuf,
local_to_target: PathBuf,
target_is_file: bool,
}
impl Entry {
fn new(context: &Context, direntry: &DirEntry) -> Result<Self, StripPrefixError> {
let source_relative = direntry.path().to_path_buf();
let source_absolute = context.current_dir.join(&source_relative);
let descendant =
get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?;
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,
})
}
}
fn ends_with_slash_dot<P>(path: P) -> bool
where
P: AsRef<Path>,
{
path.as_ref().display().to_string().ends_with("/.")
}
fn copy_direntry(
progress_bar: &Option<ProgressBar>,
entry: Entry,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
preserve_hard_links: bool,
hard_links: &mut Vec<(String, u64)>,
) -> CopyResult<()> {
let Entry {
source_absolute,
source_relative,
local_to_target,
target_is_file,
} = entry;
if source_absolute.is_symlink() && !options.dereference {
return copy_link(&source_absolute, &local_to_target, symlinked_files);
}
if source_absolute.is_dir()
&& !ends_with_slash_dot(&source_absolute)
&& !local_to_target.exists()
{
if target_is_file {
return Err("cannot overwrite non-directory with directory".into());
} else {
fs::create_dir_all(&local_to_target)?;
if options.verbose {
println!("{}", context_for(&source_relative, &local_to_target));
}
return Ok(());
}
}
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() {
Ok(())
} else {
Err(err)
}
}
}?;
}
} else {
match copy_file(
progress_bar,
&source_absolute,
local_to_target.as_path(),
options,
symlinked_files,
false,
) {
Ok(_) => {}
Err(Error::IoErrContext(e, _))
if e.kind() == std::io::ErrorKind::PermissionDenied =>
{
show!(uio_error!(
e,
"cannot open {} for reading",
source_relative.quote(),
));
}
Err(e) => return Err(e),
}
}
}
Ok(())
}
pub(crate) fn copy_directory(
progress_bar: &Option<ProgressBar>,
root: &Path,
target: &Path,
options: &Options,
symlinked_files: &mut HashSet<FileInformation>,
source_in_command_line: bool,
) -> CopyResult<()> {
if !options.recursive {
return Err(format!("omitting directory {}", root.quote()).into());
}
if !options.dereference(source_in_command_line) && root.is_symlink() {
return copy_file(
progress_bar,
root,
target,
options,
symlinked_files,
source_in_command_line,
);
}
if path_has_prefix(target, root)? {
return Err(format!(
"cannot copy a directory, {}, into itself, {}",
root.quote(),
target.join(root.file_name().unwrap()).quote()
)
.into());
}
let tmp = if options.parents {
if let Some(parent) = root.parent() {
let new_target = target.join(parent);
std::fs::create_dir_all(&new_target)?;
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 mut hard_links: Vec<(String, u64)> = vec![];
let preserve_hard_links = options.preserve_hard_links();
let context = match Context::new(root, target) {
Ok(c) => c,
Err(e) => return Err(format!("failed to get current directory {e}").into()),
};
for direntry_result in WalkDir::new(root)
.same_file_system(options.one_file_system)
.follow_links(options.dereference)
{
match direntry_result {
Ok(direntry) => {
let entry = Entry::new(&context, &direntry)?;
copy_direntry(
progress_bar,
entry,
options,
symlinked_files,
preserve_hard_links,
&mut hard_links,
)?;
}
Err(e) => show_error!("{}", e),
}
}
if options.parents {
let dest = target.join(root.file_name().unwrap());
copy_attributes(root, dest.as_path(), &options.attributes)?;
for (x, y) in aligned_ancestors(root, dest.as_path()) {
copy_attributes(x, y, &options.attributes)?;
}
} else {
copy_attributes(root, target, &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))
}
#[cfg(test)]
mod tests {
use super::ends_with_slash_dot;
#[test]
fn test_ends_with_slash_dot() {
assert!(ends_with_slash_dot("/."));
assert!(ends_with_slash_dot("./."));
assert!(ends_with_slash_dot("../."));
assert!(ends_with_slash_dot("a/."));
assert!(ends_with_slash_dot("/a/."));
assert!(!ends_with_slash_dot(""));
assert!(!ends_with_slash_dot("."));
assert!(!ends_with_slash_dot("./"));
assert!(!ends_with_slash_dot(".."));
assert!(!ends_with_slash_dot("/.."));
assert!(!ends_with_slash_dot("a/.."));
assert!(!ends_with_slash_dot("/a/.."));
}
}