use std::io;
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(target_os = "linux")]
static FICLONE_UNSUPPORTED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DerefMode {
Never,
CommandLine,
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackupMode {
Numbered,
Existing,
Simple,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReflinkMode {
Auto,
Always,
Never,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SparseMode {
Auto,
Always,
Never,
}
pub struct CpConfig {
pub recursive: bool,
pub force: bool,
pub interactive: bool,
pub no_clobber: bool,
pub verbose: bool,
pub preserve_mode: bool,
pub preserve_ownership: bool,
pub preserve_timestamps: bool,
pub dereference: DerefMode,
pub link: bool,
pub symbolic_link: bool,
pub update: bool,
pub one_file_system: bool,
pub backup: Option<BackupMode>,
pub suffix: String,
pub reflink: ReflinkMode,
pub target_directory: Option<String>,
pub no_target_directory: bool,
pub strip_trailing_slashes: bool,
pub attributes_only: bool,
pub parents: bool,
pub sparse: SparseMode,
}
impl Default for CpConfig {
fn default() -> Self {
Self {
recursive: false,
force: false,
interactive: false,
no_clobber: false,
verbose: false,
preserve_mode: false,
preserve_ownership: false,
preserve_timestamps: false,
dereference: DerefMode::CommandLine,
link: false,
symbolic_link: false,
update: false,
one_file_system: false,
backup: None,
suffix: "~".to_string(),
reflink: ReflinkMode::Auto,
target_directory: None,
no_target_directory: false,
strip_trailing_slashes: false,
attributes_only: false,
parents: false,
sparse: SparseMode::Auto,
}
}
}
pub fn parse_sparse_mode(s: &str) -> Result<SparseMode, String> {
match s {
"auto" => Ok(SparseMode::Auto),
"always" => Ok(SparseMode::Always),
"never" => Ok(SparseMode::Never),
_ => Err(format!("invalid argument '{}' for '--sparse'", s)),
}
}
pub fn apply_no_preserve(list: &str, config: &mut CpConfig) {
for attr in list.split(',') {
match attr.trim() {
"mode" => config.preserve_mode = false,
"ownership" => config.preserve_ownership = false,
"timestamps" => config.preserve_timestamps = false,
"links" | "context" | "xattr" => { }
"all" => {
config.preserve_mode = false;
config.preserve_ownership = false;
config.preserve_timestamps = false;
}
_ => {}
}
}
}
pub fn parse_backup_mode(s: &str) -> Result<BackupMode, String> {
match s {
"none" | "off" => Ok(BackupMode::None),
"numbered" | "t" => Ok(BackupMode::Numbered),
"existing" | "nil" => Ok(BackupMode::Existing),
"simple" | "never" => Ok(BackupMode::Simple),
_ => Err(format!("invalid backup type '{}'", s)),
}
}
pub fn parse_reflink_mode(s: &str) -> Result<ReflinkMode, String> {
match s {
"auto" => Ok(ReflinkMode::Auto),
"always" => Ok(ReflinkMode::Always),
"never" => Ok(ReflinkMode::Never),
_ => Err(format!("invalid reflink value '{}'", s)),
}
}
pub fn apply_preserve(list: &str, config: &mut CpConfig) {
for attr in list.split(',') {
match attr.trim() {
"mode" => config.preserve_mode = true,
"ownership" => config.preserve_ownership = true,
"timestamps" => config.preserve_timestamps = true,
"links" | "context" | "xattr" => { }
"all" => {
config.preserve_mode = true;
config.preserve_ownership = true;
config.preserve_timestamps = true;
}
_ => {}
}
}
}
fn make_backup(dst: &Path, config: &CpConfig) -> io::Result<()> {
let mode = match config.backup {
Some(m) => m,
None => return Ok(()),
};
if mode == BackupMode::None {
return Ok(());
}
if !dst.exists() {
return Ok(());
}
let backup_path = match mode {
BackupMode::Simple | BackupMode::None => {
let mut p = dst.as_os_str().to_os_string();
p.push(&config.suffix);
std::path::PathBuf::from(p)
}
BackupMode::Numbered => numbered_backup_path(dst),
BackupMode::Existing => {
let numbered = numbered_backup_candidate(dst, 1);
if numbered.exists() {
numbered_backup_path(dst)
} else {
let mut p = dst.as_os_str().to_os_string();
p.push(&config.suffix);
std::path::PathBuf::from(p)
}
}
};
std::fs::rename(dst, &backup_path)?;
Ok(())
}
fn numbered_backup_path(dst: &Path) -> std::path::PathBuf {
let mut n: u64 = 1;
loop {
let candidate = numbered_backup_candidate(dst, n);
if !candidate.exists() {
return candidate;
}
n += 1;
}
}
fn numbered_backup_candidate(dst: &Path, n: u64) -> std::path::PathBuf {
let mut p = dst.as_os_str().to_os_string();
p.push(format!(".~{}~", n));
std::path::PathBuf::from(p)
}
fn preserve_attributes_from_meta(
meta: &std::fs::Metadata,
dst: &Path,
config: &CpConfig,
) -> io::Result<()> {
#[cfg(unix)]
if config.preserve_mode {
let mode = meta.mode();
std::fs::set_permissions(dst, std::fs::Permissions::from_mode(mode))?;
}
#[cfg(unix)]
if config.preserve_timestamps {
let atime_spec = libc::timespec {
tv_sec: meta.atime(),
tv_nsec: meta.atime_nsec(),
};
let mtime_spec = libc::timespec {
tv_sec: meta.mtime(),
tv_nsec: meta.mtime_nsec(),
};
let times = [atime_spec, mtime_spec];
let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
if ret != 0 {
return Err(io::Error::last_os_error());
}
}
#[cfg(unix)]
if config.preserve_ownership {
let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let ret = unsafe { libc::lchown(c_path.as_ptr(), meta.uid(), meta.gid()) };
if ret != 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EPERM) {
return Err(err);
}
}
}
#[cfg(not(unix))]
{
let _ = (meta, config);
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn copy_data_large_buf(src: &Path, dst: &Path, src_len: u64, src_mode: u32) -> io::Result<()> {
use std::cell::RefCell;
use std::io::{Read, Write};
const MAX_BUF: usize = 4 * 1024 * 1024; const SHRINK_THRESHOLD: usize = 512 * 1024;
thread_local! {
static BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
let buf_size = src_len.min(MAX_BUF as u64).max(8192) as usize;
let mut reader = std::fs::File::open(src)?;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(src_mode);
}
#[cfg(not(unix))]
let _ = src_mode;
let mut writer = opts.open(dst)?;
BUF.with(|cell| {
let mut buf = cell.borrow_mut();
if buf.len() > SHRINK_THRESHOLD && buf_size < buf.len() / 4 {
buf.resize(buf_size, 0);
buf.shrink_to_fit();
} else if buf.len() < buf_size {
buf.resize(buf_size, 0);
}
loop {
let n = reader.read(&mut buf[..buf_size])?;
if n == 0 {
break;
}
writer.write_all(&buf[..n])?;
}
Ok(())
})
}
#[cfg(target_os = "linux")]
fn copy_data_linux(src: &Path, dst: &Path, config: &CpConfig, create_mode: u32) -> io::Result<()> {
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
let src_file = std::fs::File::open(src)?;
let src_fd = src_file.as_raw_fd();
let fd_meta = src_file.metadata()?;
let len = fd_meta.len();
let dst_file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(create_mode)
.open(dst)?;
let dst_fd = dst_file.as_raw_fd();
unsafe {
let _ = libc::posix_fadvise(src_fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
}
if matches!(config.reflink, ReflinkMode::Auto | ReflinkMode::Always) {
const FICLONE: libc::c_ulong = 0x40049409;
let should_try = config.reflink == ReflinkMode::Always
|| !FICLONE_UNSUPPORTED.load(std::sync::atomic::Ordering::Relaxed);
if should_try {
let ret = unsafe { libc::ioctl(dst_fd, FICLONE, src_fd) };
if ret == 0 {
return Ok(());
}
let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
if config.reflink == ReflinkMode::Always {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"failed to clone '{}' to '{}': {}",
src.display(),
dst.display(),
io::Error::from_raw_os_error(errno)
),
));
}
if matches!(errno, libc::EOPNOTSUPP | libc::ENOTTY | libc::ENOSYS) {
FICLONE_UNSUPPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
}
if errno == libc::EXDEV {
return readwrite_with_buffer(src_file, dst_file, len);
}
}
}
let mut remaining = match i64::try_from(len) {
Ok(v) => v,
Err(_) => return readwrite_with_buffer(src_file, dst_file, len),
};
let mut cfr_failed = false;
while remaining > 0 {
let to_copy = (remaining as u64).min(isize::MAX as u64) as usize;
let ret = unsafe {
libc::syscall(
libc::SYS_copy_file_range,
src_fd,
std::ptr::null_mut::<libc::off64_t>(),
dst_fd,
std::ptr::null_mut::<libc::off64_t>(),
to_copy,
0u32,
)
};
if ret < 0 {
let err = io::Error::last_os_error();
if matches!(
err.raw_os_error(),
Some(libc::EINVAL | libc::ENOSYS | libc::EXDEV)
) {
cfr_failed = true;
break;
}
return Err(err);
}
if ret == 0 {
if remaining > 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"source file shrank during copy",
));
}
break;
}
remaining -= ret as i64;
}
if !cfr_failed {
return Ok(());
}
use std::io::Seek;
let mut src_file = src_file;
let mut dst_file = dst_file;
src_file.seek(std::io::SeekFrom::Start(0))?;
dst_file.seek(std::io::SeekFrom::Start(0))?;
dst_file.set_len(0)?;
readwrite_with_buffer(src_file, dst_file, len)
}
#[cfg(target_os = "linux")]
fn readwrite_with_buffer(
mut src_file: std::fs::File,
mut dst_file: std::fs::File,
len: u64,
) -> io::Result<()> {
use std::cell::RefCell;
use std::io::{Read, Write};
const MAX_BUF: usize = 4 * 1024 * 1024;
const SHRINK_THRESHOLD: usize = 512 * 1024;
let buf_size = (len.min(MAX_BUF as u64) as usize).max(8192);
thread_local! {
static BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
BUF.with(|cell| {
let mut buf = cell.borrow_mut();
if buf.len() > SHRINK_THRESHOLD && buf_size < buf.len() / 4 {
buf.resize(buf_size, 0);
buf.shrink_to_fit();
} else if buf.len() < buf_size {
buf.resize(buf_size, 0);
}
loop {
let n = src_file.read(&mut buf[..buf_size])?;
if n == 0 {
break;
}
dst_file.write_all(&buf[..n])?;
}
Ok(())
})
}
pub fn copy_file(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
let src_meta = if config.dereference == DerefMode::Always {
std::fs::metadata(src)?
} else {
std::fs::symlink_metadata(src)?
};
copy_file_with_meta(src, dst, &src_meta, config)
}
fn copy_file_with_meta(
src: &Path,
dst: &Path,
src_meta: &std::fs::Metadata,
config: &CpConfig,
) -> io::Result<()> {
if src_meta.file_type().is_symlink() && config.dereference == DerefMode::Never {
let target = std::fs::read_link(src)?;
#[cfg(unix)]
{
std::os::unix::fs::symlink(&target, dst)?;
}
#[cfg(not(unix))]
{
let _ = target;
std::fs::copy(src, dst)?;
}
return Ok(());
}
if config.attributes_only {
if !dst.exists() {
std::fs::File::create(dst)?;
}
preserve_attributes_from_meta(src_meta, dst, config)?;
return Ok(());
}
if config.link {
std::fs::hard_link(src, dst)?;
return Ok(());
}
if config.symbolic_link {
#[cfg(unix)]
{
std::os::unix::fs::symlink(src, dst)?;
}
#[cfg(not(unix))]
{
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"symbolic links are not supported on this platform",
));
}
return Ok(());
}
#[cfg(unix)]
let create_mode: u32 = if config.preserve_mode {
src_meta.mode()
} else {
0o666
};
#[cfg(target_os = "linux")]
{
copy_data_linux(src, dst, config, create_mode)?;
preserve_attributes_from_meta(src_meta, dst, config)?;
return Ok(());
}
#[cfg(not(target_os = "linux"))]
{
#[cfg(not(unix))]
let create_mode = 0o666u32;
copy_data_large_buf(src, dst, src_meta.len(), create_mode)?;
preserve_attributes_from_meta(src_meta, dst, config)?;
Ok(())
}
}
fn copy_recursive(
src: &Path,
dst: &Path,
config: &CpConfig,
root_dev: Option<u64>,
) -> io::Result<()> {
let src_meta = std::fs::symlink_metadata(src)?;
#[cfg(unix)]
if config.one_file_system {
if let Some(dev) = root_dev {
if src_meta.dev() != dev {
return Ok(());
}
}
}
if src_meta.is_dir() {
if !dst.exists() {
std::fs::create_dir_all(dst)?;
}
#[cfg(unix)]
let next_dev = Some(root_dev.unwrap_or(src_meta.dev()));
#[cfg(not(unix))]
let next_dev: Option<u64> = None;
let mut files: Vec<(std::path::PathBuf, std::path::PathBuf, std::fs::Metadata)> =
Vec::new();
let mut dirs: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new();
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let child_src = entry.path();
let child_dst = dst.join(entry.file_name());
let meta = if config.dereference == DerefMode::Always {
std::fs::metadata(&child_src)?
} else {
std::fs::symlink_metadata(&child_src)?
};
#[cfg(unix)]
if config.one_file_system {
if let Some(dev) = root_dev {
if meta.dev() != dev {
continue;
}
}
}
if meta.is_dir() {
dirs.push((child_src, child_dst));
} else {
files.push((child_src, child_dst, meta));
}
}
const PARALLEL_FILE_THRESHOLD: usize = 8;
if files.len() >= PARALLEL_FILE_THRESHOLD {
use rayon::prelude::*;
let result: Result<(), io::Error> =
files
.par_iter()
.try_for_each(|(child_src, child_dst, meta)| {
copy_file_with_meta(child_src, child_dst, meta, config)
});
result?;
} else {
for (child_src, child_dst, meta) in &files {
copy_file_with_meta(child_src, child_dst, meta, config)?;
}
}
for (child_src, child_dst) in &dirs {
copy_recursive(child_src, child_dst, config, next_dev)?;
}
preserve_attributes_from_meta(&src_meta, dst, config)?;
} else {
if let Some(parent) = dst.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
copy_file_with_meta(src, dst, &src_meta, config)?;
}
Ok(())
}
pub fn run_cp(
sources: &[String],
raw_dest: Option<&str>,
config: &CpConfig,
) -> (Vec<String>, bool) {
let mut errors: Vec<String> = Vec::new();
let mut had_error = false;
let dest_dir: Option<std::path::PathBuf> = config
.target_directory
.as_deref()
.or(raw_dest)
.map(std::path::PathBuf::from);
let dest_dir = match dest_dir {
Some(d) => d,
None => {
errors.push("cp: missing destination operand".to_string());
return (errors, true);
}
};
let copy_into_dir = sources.len() > 1 || dest_dir.is_dir() || config.target_directory.is_some();
let copy_into_dir = copy_into_dir && !config.no_target_directory;
for source in sources {
let src_str = if config.strip_trailing_slashes {
source.trim_end_matches('/')
} else {
source.as_str()
};
let src = Path::new(src_str);
let dst = if config.parents {
let rel = src_str.trim_start_matches('/');
dest_dir.join(rel)
} else if copy_into_dir {
let name = src.file_name().unwrap_or(src.as_ref());
dest_dir.join(name)
} else {
dest_dir.clone()
};
if config.parents {
if let Some(parent) = dst.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
let inner = strip_os_error(&e);
errors.push(format!(
"cp: cannot create directory '{}': {}",
parent.display(),
inner
));
had_error = true;
continue;
}
}
}
}
if let Err(e) = do_copy(src, &dst, config) {
let inner = strip_os_error(&e);
let msg = if inner.contains("are the same file") {
format!("cp: {}", inner)
} else if inner.contains("omitting directory") {
format!("cp: {}", inner)
} else {
format!(
"cp: cannot copy '{}' to '{}': {}",
src.display(),
dst.display(),
inner
)
};
errors.push(msg);
had_error = true;
} else if config.verbose {
println!("'{}' -> '{}'", src.display(), dst.display());
}
}
(errors, had_error)
}
fn do_copy(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
let src_meta = if config.dereference == DerefMode::Always {
std::fs::metadata(src)?
} else {
std::fs::symlink_metadata(src)?
};
if src_meta.is_dir() && !config.recursive {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("omitting directory '{}'", src.display()),
));
}
#[cfg(unix)]
if src_meta.is_dir() && dst.exists() && !dst.is_dir() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"cannot overwrite non-directory '{}' with directory '{}'",
dst.display(),
src.display()
),
));
}
if config.no_clobber && dst.exists() {
return Ok(());
}
if config.update && dst.exists() {
if let (Ok(src_m), Ok(dst_m)) = (src.metadata(), dst.metadata()) {
if let (Ok(src_t), Ok(dst_t)) = (src_m.modified(), dst_m.modified()) {
if dst_t >= src_t {
return Ok(());
}
}
}
}
if config.interactive && dst.exists() {
eprint!("cp: overwrite '{}'? ", dst.display());
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let r = response.trim().to_lowercase();
if !(r == "y" || r == "yes") {
return Ok(());
}
}
#[cfg(unix)]
if !src_meta.is_dir() && dst.exists() {
if let Ok(dst_meta) = std::fs::metadata(dst) {
if src_meta.dev() == dst_meta.dev() && src_meta.ino() == dst_meta.ino() {
let has_backup = matches!(
config.backup,
Some(BackupMode::Simple | BackupMode::Numbered | BackupMode::Existing)
);
if has_backup {
make_backup(dst, config)?;
let backup_src = match config.backup.unwrap() {
BackupMode::Simple | BackupMode::None => {
let mut p = dst.as_os_str().to_os_string();
p.push(&config.suffix);
std::path::PathBuf::from(p)
}
BackupMode::Numbered => {
let mut n: u64 = 1;
loop {
let candidate = numbered_backup_candidate(dst, n);
let next = numbered_backup_candidate(dst, n + 1);
if !next.exists() {
break candidate;
}
n += 1;
}
}
BackupMode::Existing => {
let numbered = numbered_backup_candidate(dst, 1);
if numbered.exists() {
let mut n: u64 = 1;
loop {
let candidate = numbered_backup_candidate(dst, n);
let next = numbered_backup_candidate(dst, n + 1);
if !next.exists() {
break candidate;
}
n += 1;
}
} else {
let mut p = dst.as_os_str().to_os_string();
p.push(&config.suffix);
std::path::PathBuf::from(p)
}
}
};
return copy_file(&backup_src, dst, config);
}
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"'{}' and '{}' are the same file",
src.display(),
dst.display()
),
));
}
}
}
if config.force && dst.exists() {
if config.link || config.symbolic_link {
if dst.is_dir() {
std::fs::remove_dir(dst).ok();
} else {
std::fs::remove_file(dst).ok();
}
} else if let Ok(m) = dst.metadata() {
if m.permissions().readonly() {
std::fs::remove_file(dst)?;
}
}
}
if !src_meta.is_dir() {
make_backup(dst, config)?;
}
if src_meta.is_dir() {
#[cfg(unix)]
let root_dev = Some(src_meta.dev());
#[cfg(not(unix))]
let root_dev: Option<u64> = None;
copy_recursive(src, dst, config, root_dev)
} else {
copy_file(src, dst, config)
}
}
fn strip_os_error(e: &io::Error) -> String {
if let Some(raw) = e.raw_os_error() {
let msg = format!("{}", e);
msg.replace(&format!(" (os error {})", raw), "")
} else {
format!("{}", e)
}
}