Skip to main content

coreutils_rs/cp/
core.rs

1use std::io;
2use std::path::Path;
3
4#[cfg(unix)]
5use std::os::unix::fs::MetadataExt;
6#[cfg(unix)]
7use std::os::unix::fs::PermissionsExt;
8
9// FICLONE support cache: avoids repeated failed ioctl attempts on non-reflink filesystems.
10// NOTE: this is per-process with no filesystem identity — it assumes all copies within a
11// single invocation target the same destination filesystem. A cross-filesystem recursive
12// copy (e.g. btrfs + ext4 mount points) may suppress FICLONE on the reflink-capable fs
13// after a failure on the non-reflink fs. This matches GNU cp's practical usage pattern
14// where --reflink=auto targets a single destination tree.
15#[cfg(target_os = "linux")]
16static FICLONE_UNSUPPORTED: std::sync::atomic::AtomicBool =
17    std::sync::atomic::AtomicBool::new(false);
18
19/// How to dereference (follow) symbolic links.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum DerefMode {
22    /// Never follow symlinks (copy the link itself).
23    Never,
24    /// Follow symlinks given on the command line, but not encountered during recursion.
25    CommandLine,
26    /// Always follow symlinks.
27    Always,
28}
29
30/// Backup strategy, following GNU `--backup` semantics.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum BackupMode {
33    /// Numbered backups (~1~, ~2~, ...).
34    Numbered,
35    /// Numbered if numbered backups already exist, otherwise simple.
36    Existing,
37    /// Simple backup with suffix.
38    Simple,
39    /// Never make backups.
40    None,
41}
42
43/// Reflink (copy-on-write clone) strategy.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ReflinkMode {
46    /// Try reflink, fall back to normal copy.
47    Auto,
48    /// Require reflink; fail if not supported.
49    Always,
50    /// Never attempt reflink.
51    Never,
52}
53
54/// Sparse file copy strategy.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum SparseMode {
57    /// Detect sparse files and create sparse output.
58    Auto,
59    /// Always try to create sparse output.
60    Always,
61    /// Never create sparse output.
62    Never,
63}
64
65/// Configuration for a cp invocation.
66pub struct CpConfig {
67    pub recursive: bool,
68    pub force: bool,
69    pub interactive: bool,
70    pub no_clobber: bool,
71    pub verbose: bool,
72    pub preserve_mode: bool,
73    pub preserve_ownership: bool,
74    pub preserve_timestamps: bool,
75    pub dereference: DerefMode,
76    pub link: bool,
77    pub symbolic_link: bool,
78    pub update: bool,
79    pub one_file_system: bool,
80    pub backup: Option<BackupMode>,
81    pub suffix: String,
82    pub reflink: ReflinkMode,
83    pub target_directory: Option<String>,
84    pub no_target_directory: bool,
85    pub strip_trailing_slashes: bool,
86    pub attributes_only: bool,
87    pub parents: bool,
88    pub sparse: SparseMode,
89}
90
91impl Default for CpConfig {
92    fn default() -> Self {
93        Self {
94            recursive: false,
95            force: false,
96            interactive: false,
97            no_clobber: false,
98            verbose: false,
99            preserve_mode: false,
100            preserve_ownership: false,
101            preserve_timestamps: false,
102            dereference: DerefMode::CommandLine,
103            link: false,
104            symbolic_link: false,
105            update: false,
106            one_file_system: false,
107            backup: None,
108            suffix: "~".to_string(),
109            reflink: ReflinkMode::Auto,
110            target_directory: None,
111            no_target_directory: false,
112            strip_trailing_slashes: false,
113            attributes_only: false,
114            parents: false,
115            sparse: SparseMode::Auto,
116        }
117    }
118}
119
120/// Parse a `--sparse=WHEN` value.
121pub fn parse_sparse_mode(s: &str) -> Result<SparseMode, String> {
122    match s {
123        "auto" => Ok(SparseMode::Auto),
124        "always" => Ok(SparseMode::Always),
125        "never" => Ok(SparseMode::Never),
126        _ => Err(format!("invalid argument '{}' for '--sparse'", s)),
127    }
128}
129
130/// Parse a `--no-preserve=LIST` attribute list. Unsets the corresponding flags.
131pub fn apply_no_preserve(list: &str, config: &mut CpConfig) {
132    for attr in list.split(',') {
133        match attr.trim() {
134            "mode" => config.preserve_mode = false,
135            "ownership" => config.preserve_ownership = false,
136            "timestamps" => config.preserve_timestamps = false,
137            "links" | "context" | "xattr" => { /* acknowledged */ }
138            "all" => {
139                config.preserve_mode = false;
140                config.preserve_ownership = false;
141                config.preserve_timestamps = false;
142            }
143            _ => {}
144        }
145    }
146}
147
148/// Parse a `--backup=CONTROL` value.
149pub fn parse_backup_mode(s: &str) -> Result<BackupMode, String> {
150    match s {
151        "none" | "off" => Ok(BackupMode::None),
152        "numbered" | "t" => Ok(BackupMode::Numbered),
153        "existing" | "nil" => Ok(BackupMode::Existing),
154        "simple" | "never" => Ok(BackupMode::Simple),
155        _ => Err(format!("invalid backup type '{}'", s)),
156    }
157}
158
159/// Parse a `--reflink[=WHEN]` value.
160pub fn parse_reflink_mode(s: &str) -> Result<ReflinkMode, String> {
161    match s {
162        "auto" => Ok(ReflinkMode::Auto),
163        "always" => Ok(ReflinkMode::Always),
164        "never" => Ok(ReflinkMode::Never),
165        _ => Err(format!("invalid reflink value '{}'", s)),
166    }
167}
168
169/// Parse a `--preserve[=LIST]` attribute list.
170///
171/// Supports: mode, ownership, timestamps, links, context, xattr, all.
172pub fn apply_preserve(list: &str, config: &mut CpConfig) {
173    for attr in list.split(',') {
174        match attr.trim() {
175            "mode" => config.preserve_mode = true,
176            "ownership" => config.preserve_ownership = true,
177            "timestamps" => config.preserve_timestamps = true,
178            "links" | "context" | "xattr" => { /* acknowledged but not yet implemented */ }
179            "all" => {
180                config.preserve_mode = true;
181                config.preserve_ownership = true;
182                config.preserve_timestamps = true;
183            }
184            _ => {}
185        }
186    }
187}
188
189// ---- backup helpers ----
190
191/// Create a backup of `dst` if it exists, according to the configured backup mode.
192/// Returns `Ok(())` when no backup is needed or the backup was made successfully.
193fn make_backup(dst: &Path, config: &CpConfig) -> io::Result<()> {
194    let mode = match config.backup {
195        Some(m) => m,
196        None => return Ok(()),
197    };
198    if mode == BackupMode::None {
199        return Ok(());
200    }
201    if !dst.exists() {
202        return Ok(());
203    }
204
205    let backup_path = match mode {
206        BackupMode::Simple | BackupMode::None => {
207            let mut p = dst.as_os_str().to_os_string();
208            p.push(&config.suffix);
209            std::path::PathBuf::from(p)
210        }
211        BackupMode::Numbered => numbered_backup_path(dst),
212        BackupMode::Existing => {
213            // Use numbered if any numbered backup already exists.
214            let numbered = numbered_backup_candidate(dst, 1);
215            if numbered.exists() {
216                numbered_backup_path(dst)
217            } else {
218                let mut p = dst.as_os_str().to_os_string();
219                p.push(&config.suffix);
220                std::path::PathBuf::from(p)
221            }
222        }
223    };
224
225    std::fs::rename(dst, &backup_path)?;
226    Ok(())
227}
228
229fn numbered_backup_path(dst: &Path) -> std::path::PathBuf {
230    let mut n: u64 = 1;
231    loop {
232        let candidate = numbered_backup_candidate(dst, n);
233        if !candidate.exists() {
234            return candidate;
235        }
236        n += 1;
237    }
238}
239
240fn numbered_backup_candidate(dst: &Path, n: u64) -> std::path::PathBuf {
241    let mut p = dst.as_os_str().to_os_string();
242    p.push(format!(".~{}~", n));
243    std::path::PathBuf::from(p)
244}
245
246// ---- attribute preservation ----
247
248/// Preserve file attributes (mode, timestamps, ownership) on `dst` using
249/// pre-fetched source metadata (avoids redundant stat calls).
250fn preserve_attributes_from_meta(
251    meta: &std::fs::Metadata,
252    dst: &Path,
253    config: &CpConfig,
254) -> io::Result<()> {
255    // Only chmod when -p/--preserve=mode is set. Without it, the destination
256    // keeps its O_CREAT permissions (source_mode & ~umask), matching GNU cp.
257    #[cfg(unix)]
258    if config.preserve_mode {
259        let mode = meta.mode();
260        std::fs::set_permissions(dst, std::fs::Permissions::from_mode(mode))?;
261    }
262
263    #[cfg(unix)]
264    if config.preserve_timestamps {
265        let atime_spec = libc::timespec {
266            tv_sec: meta.atime(),
267            tv_nsec: meta.atime_nsec(),
268        };
269        let mtime_spec = libc::timespec {
270            tv_sec: meta.mtime(),
271            tv_nsec: meta.mtime_nsec(),
272        };
273        let times = [atime_spec, mtime_spec];
274        // SAFETY: CString::new checks for interior NULs; the path is valid UTF-8/bytes.
275        let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
276            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
277        // SAFETY: c_path is a valid NUL-terminated C string, times is a valid [timespec; 2].
278        let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
279        if ret != 0 {
280            return Err(io::Error::last_os_error());
281        }
282    }
283
284    #[cfg(unix)]
285    if config.preserve_ownership {
286        // SAFETY: CString::new checks for interior NULs; the path is valid bytes.
287        let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
288            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
289        // SAFETY: c_path is a valid NUL-terminated C string, uid/gid are valid u32 values.
290        let ret = unsafe { libc::lchown(c_path.as_ptr(), meta.uid(), meta.gid()) };
291        if ret != 0 {
292            // Ownership preservation may fail for non-root; ignore EPERM.
293            let err = io::Error::last_os_error();
294            if err.raw_os_error() != Some(libc::EPERM) {
295                return Err(err);
296            }
297        }
298    }
299
300    // Suppress unused-variable warnings on non-unix platforms.
301    #[cfg(not(unix))]
302    {
303        let _ = (meta, config);
304    }
305
306    Ok(())
307}
308
309// ---- large-buffer fallback copy ----
310
311/// Copy file data using a thread-local buffer (up to 4MB, capped to file size).
312/// Avoids stdlib's 64KB default buffer and amortizes allocation across files.
313/// Creates the destination with `src_mode` so the kernel applies the process umask.
314/// Used on non-Linux platforms; Linux uses `copy_data_linux` instead.
315#[cfg(not(target_os = "linux"))]
316fn copy_data_large_buf(src: &Path, dst: &Path, src_len: u64, src_mode: u32) -> io::Result<()> {
317    use std::cell::RefCell;
318    use std::io::{Read, Write};
319    const MAX_BUF: usize = 4 * 1024 * 1024; // 4 MB
320    /// Shrink the thread-local buffer when it exceeds this size and the current
321    /// file needs much less, to avoid holding 4 MB per Rayon thread permanently.
322    const SHRINK_THRESHOLD: usize = 512 * 1024; // 512 KB
323
324    thread_local! {
325        static BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
326    }
327
328    // Safe on 32-bit: clamp via u64 before casting to usize.
329    let buf_size = src_len.min(MAX_BUF as u64).max(8192) as usize;
330
331    let mut reader = std::fs::File::open(src)?;
332    let mut opts = std::fs::OpenOptions::new();
333    opts.write(true).create(true).truncate(true);
334    #[cfg(unix)]
335    {
336        use std::os::unix::fs::OpenOptionsExt;
337        opts.mode(src_mode);
338    }
339    #[cfg(not(unix))]
340    let _ = src_mode;
341    let mut writer = opts.open(dst)?;
342
343    BUF.with(|cell| {
344        let mut buf = cell.borrow_mut();
345        // Shrink if buffer is much larger than needed to limit per-thread memory.
346        if buf.len() > SHRINK_THRESHOLD && buf_size < buf.len() / 4 {
347            buf.resize(buf_size, 0);
348            buf.shrink_to_fit();
349        } else if buf.len() < buf_size {
350            buf.resize(buf_size, 0);
351        }
352        loop {
353            let n = reader.read(&mut buf[..buf_size])?;
354            if n == 0 {
355                break;
356            }
357            writer.write_all(&buf[..n])?;
358        }
359        Ok(())
360    })
361}
362
363// ---- Linux single-open cascade copy ----
364//
365// Opens src and dst once, then tries FICLONE → copy_file_range → read/write
366// on the same file descriptors. Eliminates redundant open/close/stat syscalls
367// that the old code paid when FICLONE failed on non-reflink filesystems.
368
369#[cfg(target_os = "linux")]
370fn copy_data_linux(src: &Path, dst: &Path, config: &CpConfig, create_mode: u32) -> io::Result<()> {
371    use std::os::unix::fs::OpenOptionsExt;
372    use std::os::unix::io::AsRawFd;
373
374    let src_file = std::fs::File::open(src)?;
375    let src_fd = src_file.as_raw_fd();
376
377    // Use fstat on the opened fd (not src_meta) to get the real file size.
378    let fd_meta = src_file.metadata()?;
379    let len = fd_meta.len();
380
381    let dst_file = std::fs::OpenOptions::new()
382        .write(true)
383        .create(true)
384        .truncate(true)
385        .mode(create_mode)
386        .open(dst)?;
387    let dst_fd = dst_file.as_raw_fd();
388
389    // Hint sequential access for kernel readahead (benefits copy_file_range and read/write).
390    // posix_fadvise is advisory; failure (e.g. ESPIPE for pipes) is harmless.
391    unsafe {
392        let _ = libc::posix_fadvise(src_fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
393    }
394
395    // Step 1: Try FICLONE (instant CoW clone on btrfs/XFS).
396    if matches!(config.reflink, ReflinkMode::Auto | ReflinkMode::Always) {
397        const FICLONE: libc::c_ulong = 0x40049409;
398        let should_try = config.reflink == ReflinkMode::Always
399            || !FICLONE_UNSUPPORTED.load(std::sync::atomic::Ordering::Relaxed);
400
401        if should_try {
402            // SAFETY: src_fd and dst_fd are valid open file descriptors.
403            let ret = unsafe { libc::ioctl(dst_fd, FICLONE, src_fd) };
404            if ret == 0 {
405                return Ok(());
406            }
407            let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
408            if config.reflink == ReflinkMode::Always {
409                return Err(io::Error::new(
410                    io::ErrorKind::Unsupported,
411                    format!(
412                        "failed to clone '{}' to '{}': {}",
413                        src.display(),
414                        dst.display(),
415                        io::Error::from_raw_os_error(errno)
416                    ),
417                ));
418            }
419            if matches!(errno, libc::EOPNOTSUPP | libc::ENOTTY | libc::ENOSYS) {
420                FICLONE_UNSUPPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
421            }
422            if errno == libc::EXDEV {
423                // Cross-device: copy_file_range will also fail with EXDEV;
424                // skip directly to read/write (posix_fadvise already issued above).
425                return readwrite_with_buffer(src_file, dst_file, len);
426            }
427            // Auto mode: fall through to copy_file_range on the same fds.
428        }
429    }
430
431    // Step 2: Try copy_file_range (zero-copy in kernel, same fds).
432    let mut remaining = match i64::try_from(len) {
433        Ok(v) => v,
434        // File too large for copy_file_range offset arithmetic; skip to read/write.
435        Err(_) => return readwrite_with_buffer(src_file, dst_file, len),
436    };
437    let mut cfr_failed = false;
438    while remaining > 0 {
439        let to_copy = (remaining as u64).min(isize::MAX as u64) as usize;
440        // SAFETY: src_fd and dst_fd are valid open file descriptors;
441        // null offsets use and update the kernel file position.
442        let ret = unsafe {
443            libc::syscall(
444                libc::SYS_copy_file_range,
445                src_fd,
446                std::ptr::null_mut::<libc::off64_t>(),
447                dst_fd,
448                std::ptr::null_mut::<libc::off64_t>(),
449                to_copy,
450                0u32,
451            )
452        };
453        if ret < 0 {
454            let err = io::Error::last_os_error();
455            if matches!(
456                err.raw_os_error(),
457                Some(libc::EINVAL | libc::ENOSYS | libc::EXDEV)
458            ) {
459                cfr_failed = true;
460                break;
461            }
462            return Err(err);
463        }
464        if ret == 0 {
465            if remaining > 0 {
466                // Source file shrank during copy — report rather than silently truncate.
467                return Err(io::Error::new(
468                    io::ErrorKind::UnexpectedEof,
469                    "source file shrank during copy",
470                ));
471            }
472            break;
473        }
474        remaining -= ret as i64;
475    }
476    if !cfr_failed {
477        return Ok(());
478    }
479
480    // Step 3: Fallback — read/write on the same fds with large buffer.
481    // Reset file positions since copy_file_range may have partially transferred.
482    use std::io::Seek;
483    let mut src_file = src_file;
484    let mut dst_file = dst_file;
485    src_file.seek(std::io::SeekFrom::Start(0))?;
486    dst_file.seek(std::io::SeekFrom::Start(0))?;
487    dst_file.set_len(0)?;
488
489    readwrite_with_buffer(src_file, dst_file, len)
490}
491
492/// Read/write copy with thread-local buffer reuse (shared by all Linux fallback paths).
493#[cfg(target_os = "linux")]
494fn readwrite_with_buffer(
495    mut src_file: std::fs::File,
496    mut dst_file: std::fs::File,
497    len: u64,
498) -> io::Result<()> {
499    use std::cell::RefCell;
500    use std::io::{Read, Write};
501
502    const MAX_BUF: usize = 4 * 1024 * 1024;
503    /// Shrink when buffer is >512KB and 4x larger than needed (matches non-Linux path).
504    const SHRINK_THRESHOLD: usize = 512 * 1024;
505
506    // Clamp while still u64 to avoid 32-bit truncation on large files.
507    let buf_size = (len.min(MAX_BUF as u64) as usize).max(8192);
508
509    thread_local! {
510        static BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
511    }
512    BUF.with(|cell| {
513        let mut buf = cell.borrow_mut();
514        if buf.len() > SHRINK_THRESHOLD && buf_size < buf.len() / 4 {
515            buf.resize(buf_size, 0);
516            buf.shrink_to_fit();
517        } else if buf.len() < buf_size {
518            buf.resize(buf_size, 0);
519        }
520        loop {
521            let n = src_file.read(&mut buf[..buf_size])?;
522            if n == 0 {
523                break;
524            }
525            dst_file.write_all(&buf[..n])?;
526        }
527        Ok(())
528    })
529}
530
531// ---- single-file copy ----
532
533/// Copy a single file (or symlink) from `src` to `dst`.
534pub fn copy_file(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
535    let src_meta = if config.dereference == DerefMode::Always {
536        std::fs::metadata(src)?
537    } else {
538        std::fs::symlink_metadata(src)?
539    };
540
541    copy_file_with_meta(src, dst, &src_meta, config)
542}
543
544/// Copy a single file using pre-fetched metadata (avoids redundant stat).
545fn copy_file_with_meta(
546    src: &Path,
547    dst: &Path,
548    src_meta: &std::fs::Metadata,
549    config: &CpConfig,
550) -> io::Result<()> {
551    // Handle symlink when not dereferencing.
552    if src_meta.file_type().is_symlink() && config.dereference == DerefMode::Never {
553        let target = std::fs::read_link(src)?;
554        #[cfg(unix)]
555        {
556            std::os::unix::fs::symlink(&target, dst)?;
557        }
558        #[cfg(not(unix))]
559        {
560            // Fallback: try a regular copy (symlinks are not portable).
561            let _ = target;
562            std::fs::copy(src, dst)?;
563        }
564        return Ok(());
565    }
566
567    // Attributes-only: copy only metadata, not file data.
568    if config.attributes_only {
569        if !dst.exists() {
570            // Create empty file so we can set attributes
571            std::fs::File::create(dst)?;
572        }
573        preserve_attributes_from_meta(src_meta, dst, config)?;
574        return Ok(());
575    }
576
577    // Hard link mode.
578    if config.link {
579        std::fs::hard_link(src, dst)?;
580        return Ok(());
581    }
582
583    // Symbolic link mode.
584    if config.symbolic_link {
585        #[cfg(unix)]
586        {
587            std::os::unix::fs::symlink(src, dst)?;
588        }
589        #[cfg(not(unix))]
590        {
591            return Err(io::Error::new(
592                io::ErrorKind::Unsupported,
593                "symbolic links are not supported on this platform",
594            ));
595        }
596        return Ok(());
597    }
598
599    // Determine create mode: use source mode when preserving, else default 0666 (umask applies).
600    #[cfg(unix)]
601    let create_mode: u32 = if config.preserve_mode {
602        src_meta.mode()
603    } else {
604        0o666
605    };
606
607    // Linux: single-open cascade (FICLONE → copy_file_range → read/write).
608    #[cfg(target_os = "linux")]
609    {
610        copy_data_linux(src, dst, config, create_mode)?;
611        preserve_attributes_from_meta(src_meta, dst, config)?;
612        return Ok(());
613    }
614
615    // Non-Linux fallback: large-buffer copy (up to 4MB vs stdlib's 64KB).
616    #[cfg(not(target_os = "linux"))]
617    {
618        #[cfg(not(unix))]
619        let create_mode = 0o666u32;
620        copy_data_large_buf(src, dst, src_meta.len(), create_mode)?;
621        preserve_attributes_from_meta(src_meta, dst, config)?;
622        Ok(())
623    }
624}
625
626// ---- recursive copy ----
627
628/// Recursively copy `src` to `dst`, using parallel file copies within each directory.
629fn copy_recursive(
630    src: &Path,
631    dst: &Path,
632    config: &CpConfig,
633    root_dev: Option<u64>,
634) -> io::Result<()> {
635    let src_meta = std::fs::symlink_metadata(src)?;
636
637    #[cfg(unix)]
638    if config.one_file_system {
639        if let Some(dev) = root_dev {
640            if src_meta.dev() != dev {
641                return Ok(());
642            }
643        }
644    }
645
646    if src_meta.is_dir() {
647        if !dst.exists() {
648            std::fs::create_dir_all(dst)?;
649        }
650
651        #[cfg(unix)]
652        let next_dev = Some(root_dev.unwrap_or(src_meta.dev()));
653        #[cfg(not(unix))]
654        let next_dev: Option<u64> = None;
655
656        // Collect entries and partition into files and directories.
657        let mut files: Vec<(std::path::PathBuf, std::path::PathBuf, std::fs::Metadata)> =
658            Vec::new();
659        let mut dirs: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new();
660
661        for entry in std::fs::read_dir(src)? {
662            let entry = entry?;
663            let child_src = entry.path();
664            let child_dst = dst.join(entry.file_name());
665            // Respect dereference mode: follow symlinks when Always.
666            let meta = if config.dereference == DerefMode::Always {
667                std::fs::metadata(&child_src)?
668            } else {
669                std::fs::symlink_metadata(&child_src)?
670            };
671            // Check --one-file-system for all entries (not just directories).
672            #[cfg(unix)]
673            if config.one_file_system {
674                if let Some(dev) = root_dev {
675                    if meta.dev() != dev {
676                        continue;
677                    }
678                }
679            }
680            if meta.is_dir() {
681                dirs.push((child_src, child_dst));
682            } else {
683                files.push((child_src, child_dst, meta));
684            }
685        }
686
687        /// Minimum number of files before we parallelize copies within a directory.
688        /// Rayon dispatch overhead dominates below this threshold (empirical).
689        const PARALLEL_FILE_THRESHOLD: usize = 8;
690
691        // Copy files in parallel using Rayon when there are enough to benefit.
692        if files.len() >= PARALLEL_FILE_THRESHOLD {
693            use rayon::prelude::*;
694            let result: Result<(), io::Error> =
695                files
696                    .par_iter()
697                    .try_for_each(|(child_src, child_dst, meta)| {
698                        copy_file_with_meta(child_src, child_dst, meta, config)
699                    });
700            result?;
701        } else {
702            for (child_src, child_dst, meta) in &files {
703                copy_file_with_meta(child_src, child_dst, meta, config)?;
704            }
705        }
706
707        // Recurse into subdirectories sequentially (they may create dirs that
708        // need to exist before their children can be copied).
709        for (child_src, child_dst) in &dirs {
710            copy_recursive(child_src, child_dst, config, next_dev)?;
711        }
712
713        // Preserve directory attributes after copying contents.
714        preserve_attributes_from_meta(&src_meta, dst, config)?;
715    } else {
716        // If parent directory does not exist, create it.
717        if let Some(parent) = dst.parent() {
718            if !parent.exists() {
719                std::fs::create_dir_all(parent)?;
720            }
721        }
722        copy_file_with_meta(src, dst, &src_meta, config)?;
723    }
724    Ok(())
725}
726
727// ---- main entry point ----
728
729/// Determine the effective destination and perform the copy.
730///
731/// `sources` is the list of source paths; `raw_dest` is the positional destination
732/// (may be `None` when `--target-directory` is used).
733///
734/// Returns a list of per-file error messages (empty on full success) and a bool
735/// indicating whether any error occurred.
736pub fn run_cp(
737    sources: &[String],
738    raw_dest: Option<&str>,
739    config: &CpConfig,
740) -> (Vec<String>, bool) {
741    let mut errors: Vec<String> = Vec::new();
742    let mut had_error = false;
743
744    // Resolve destination directory.
745    let dest_dir: Option<std::path::PathBuf> = config
746        .target_directory
747        .as_deref()
748        .or(raw_dest)
749        .map(std::path::PathBuf::from);
750
751    let dest_dir = match dest_dir {
752        Some(d) => d,
753        None => {
754            errors.push("cp: missing destination operand".to_string());
755            return (errors, true);
756        }
757    };
758
759    // Multiple sources or target is an existing directory => copy into directory.
760    let copy_into_dir = sources.len() > 1 || dest_dir.is_dir() || config.target_directory.is_some();
761
762    // When -T is set, never treat destination as a directory.
763    let copy_into_dir = copy_into_dir && !config.no_target_directory;
764
765    for source in sources {
766        let src_str = if config.strip_trailing_slashes {
767            source.trim_end_matches('/')
768        } else {
769            source.as_str()
770        };
771        let src = Path::new(src_str);
772
773        let dst = if config.parents {
774            // --parents: preserve source path structure under destination
775            // Strip leading '/' so Path::join doesn't replace the base (GNU compat)
776            let rel = src_str.trim_start_matches('/');
777            dest_dir.join(rel)
778        } else if copy_into_dir {
779            let name = src.file_name().unwrap_or(src.as_ref());
780            dest_dir.join(name)
781        } else {
782            dest_dir.clone()
783        };
784
785        if config.parents {
786            // Create parent directories under destination
787            if let Some(parent) = dst.parent() {
788                if !parent.exists() {
789                    if let Err(e) = std::fs::create_dir_all(parent) {
790                        let inner = strip_os_error(&e);
791                        errors.push(format!(
792                            "cp: cannot create directory '{}': {}",
793                            parent.display(),
794                            inner
795                        ));
796                        had_error = true;
797                        continue;
798                    }
799                }
800            }
801        }
802
803        if let Err(e) = do_copy(src, &dst, config) {
804            let inner = strip_os_error(&e);
805            let msg = if inner.contains("are the same file") {
806                // GNU cp: "cp: 'X' and 'Y' are the same file" (no "cannot copy" prefix)
807                format!("cp: {}", inner)
808            } else if inner.contains("omitting directory") {
809                format!("cp: {}", inner)
810            } else {
811                format!(
812                    "cp: cannot copy '{}' to '{}': {}",
813                    src.display(),
814                    dst.display(),
815                    inner
816                )
817            };
818            errors.push(msg);
819            had_error = true;
820        } else if config.verbose {
821            // GNU cp -v outputs to stdout
822            println!("'{}' -> '{}'", src.display(), dst.display());
823        }
824    }
825
826    (errors, had_error)
827}
828
829/// Core copy dispatcher for a single source -> destination pair.
830fn do_copy(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
831    let src_meta = if config.dereference == DerefMode::Always {
832        std::fs::metadata(src)?
833    } else {
834        std::fs::symlink_metadata(src)?
835    };
836
837    // Reject directory source without -R.
838    if src_meta.is_dir() && !config.recursive {
839        return Err(io::Error::new(
840            io::ErrorKind::Other,
841            format!("omitting directory '{}'", src.display()),
842        ));
843    }
844
845    // Reject copying a directory over a non-directory.
846    #[cfg(unix)]
847    if src_meta.is_dir() && dst.exists() && !dst.is_dir() {
848        return Err(io::Error::new(
849            io::ErrorKind::Other,
850            format!(
851                "cannot overwrite non-directory '{}' with directory '{}'",
852                dst.display(),
853                src.display()
854            ),
855        ));
856    }
857
858    // No-clobber: skip if destination exists.
859    if config.no_clobber && dst.exists() {
860        return Ok(());
861    }
862
863    // Update: skip if destination is same age or newer.
864    if config.update && dst.exists() {
865        if let (Ok(src_m), Ok(dst_m)) = (src.metadata(), dst.metadata()) {
866            if let (Ok(src_t), Ok(dst_t)) = (src_m.modified(), dst_m.modified()) {
867                if dst_t >= src_t {
868                    return Ok(());
869                }
870            }
871        }
872    }
873
874    // Interactive: prompt on stderr.
875    if config.interactive && dst.exists() {
876        eprint!("cp: overwrite '{}'? ", dst.display());
877        let mut response = String::new();
878        io::stdin().read_line(&mut response)?;
879        let r = response.trim().to_lowercase();
880        if !(r == "y" || r == "yes") {
881            return Ok(());
882        }
883    }
884
885    // Same-file detection: must come before force removal to prevent data loss.
886    // When backup is enabled, self-copy is allowed: GNU cp renames the destination
887    // to the backup path first, then copies from the (still-open) source.
888    #[cfg(unix)]
889    if !src_meta.is_dir() && dst.exists() {
890        if let Ok(dst_meta) = std::fs::metadata(dst) {
891            if src_meta.dev() == dst_meta.dev() && src_meta.ino() == dst_meta.ino() {
892                let has_backup = matches!(
893                    config.backup,
894                    Some(BackupMode::Simple | BackupMode::Numbered | BackupMode::Existing)
895                );
896                if has_backup {
897                    // Make the backup (rename dst to backup path), then copy from backup
898                    make_backup(dst, config)?;
899                    // Build backup path to use as source
900                    let backup_src = match config.backup.unwrap() {
901                        BackupMode::Simple | BackupMode::None => {
902                            let mut p = dst.as_os_str().to_os_string();
903                            p.push(&config.suffix);
904                            std::path::PathBuf::from(p)
905                        }
906                        BackupMode::Numbered => {
907                            // Find highest numbered backup
908                            let mut n: u64 = 1;
909                            loop {
910                                let candidate = numbered_backup_candidate(dst, n);
911                                let next = numbered_backup_candidate(dst, n + 1);
912                                if !next.exists() {
913                                    break candidate;
914                                }
915                                n += 1;
916                            }
917                        }
918                        BackupMode::Existing => {
919                            let numbered = numbered_backup_candidate(dst, 1);
920                            if numbered.exists() {
921                                let mut n: u64 = 1;
922                                loop {
923                                    let candidate = numbered_backup_candidate(dst, n);
924                                    let next = numbered_backup_candidate(dst, n + 1);
925                                    if !next.exists() {
926                                        break candidate;
927                                    }
928                                    n += 1;
929                                }
930                            } else {
931                                let mut p = dst.as_os_str().to_os_string();
932                                p.push(&config.suffix);
933                                std::path::PathBuf::from(p)
934                            }
935                        }
936                    };
937                    return copy_file(&backup_src, dst, config);
938                }
939                return Err(io::Error::new(
940                    io::ErrorKind::Other,
941                    format!(
942                        "'{}' and '{}' are the same file",
943                        src.display(),
944                        dst.display()
945                    ),
946                ));
947            }
948        }
949    }
950
951    // Force: remove existing destination if it cannot be opened for writing,
952    // or when using --link/--symbolic-link (must remove existing to create new link).
953    if config.force && dst.exists() {
954        if config.link || config.symbolic_link {
955            // For link/symlink modes, must always remove existing first
956            if dst.is_dir() {
957                std::fs::remove_dir(dst).ok();
958            } else {
959                std::fs::remove_file(dst).ok();
960            }
961        } else if let Ok(m) = dst.metadata() {
962            if m.permissions().readonly() {
963                std::fs::remove_file(dst)?;
964            }
965        }
966    }
967
968    // Make backup if requested. GNU cp does not back up directories.
969    if !src_meta.is_dir() {
970        make_backup(dst, config)?;
971    }
972
973    if src_meta.is_dir() {
974        #[cfg(unix)]
975        let root_dev = Some(src_meta.dev());
976        #[cfg(not(unix))]
977        let root_dev: Option<u64> = None;
978        copy_recursive(src, dst, config, root_dev)
979    } else {
980        copy_file(src, dst, config)
981    }
982}
983
984/// Strip the " (os error N)" suffix from an io::Error for GNU-compatible messages.
985fn strip_os_error(e: &io::Error) -> String {
986    if let Some(raw) = e.raw_os_error() {
987        let msg = format!("{}", e);
988        msg.replace(&format!(" (os error {})", raw), "")
989    } else {
990        format!("{}", e)
991    }
992}