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/// How to dereference (follow) symbolic links.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DerefMode {
12    /// Never follow symlinks (copy the link itself).
13    Never,
14    /// Follow symlinks given on the command line, but not encountered during recursion.
15    CommandLine,
16    /// Always follow symlinks.
17    Always,
18}
19
20/// Backup strategy, following GNU `--backup` semantics.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum BackupMode {
23    /// Numbered backups (~1~, ~2~, ...).
24    Numbered,
25    /// Numbered if numbered backups already exist, otherwise simple.
26    Existing,
27    /// Simple backup with suffix.
28    Simple,
29    /// Never make backups.
30    None,
31}
32
33/// Reflink (copy-on-write clone) strategy.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ReflinkMode {
36    /// Try reflink, fall back to normal copy.
37    Auto,
38    /// Require reflink; fail if not supported.
39    Always,
40    /// Never attempt reflink.
41    Never,
42}
43
44/// Configuration for a cp invocation.
45pub struct CpConfig {
46    pub recursive: bool,
47    pub force: bool,
48    pub interactive: bool,
49    pub no_clobber: bool,
50    pub verbose: bool,
51    pub preserve_mode: bool,
52    pub preserve_ownership: bool,
53    pub preserve_timestamps: bool,
54    pub dereference: DerefMode,
55    pub link: bool,
56    pub symbolic_link: bool,
57    pub update: bool,
58    pub one_file_system: bool,
59    pub backup: Option<BackupMode>,
60    pub suffix: String,
61    pub reflink: ReflinkMode,
62    pub target_directory: Option<String>,
63    pub no_target_directory: bool,
64}
65
66impl Default for CpConfig {
67    fn default() -> Self {
68        Self {
69            recursive: false,
70            force: false,
71            interactive: false,
72            no_clobber: false,
73            verbose: false,
74            preserve_mode: false,
75            preserve_ownership: false,
76            preserve_timestamps: false,
77            dereference: DerefMode::CommandLine,
78            link: false,
79            symbolic_link: false,
80            update: false,
81            one_file_system: false,
82            backup: None,
83            suffix: "~".to_string(),
84            reflink: ReflinkMode::Auto,
85            target_directory: None,
86            no_target_directory: false,
87        }
88    }
89}
90
91/// Parse a `--backup=CONTROL` value.
92pub fn parse_backup_mode(s: &str) -> Result<BackupMode, String> {
93    match s {
94        "none" | "off" => Ok(BackupMode::None),
95        "numbered" | "t" => Ok(BackupMode::Numbered),
96        "existing" | "nil" => Ok(BackupMode::Existing),
97        "simple" | "never" => Ok(BackupMode::Simple),
98        _ => Err(format!("invalid backup type '{}'", s)),
99    }
100}
101
102/// Parse a `--reflink[=WHEN]` value.
103pub fn parse_reflink_mode(s: &str) -> Result<ReflinkMode, String> {
104    match s {
105        "auto" => Ok(ReflinkMode::Auto),
106        "always" => Ok(ReflinkMode::Always),
107        "never" => Ok(ReflinkMode::Never),
108        _ => Err(format!("invalid reflink value '{}'", s)),
109    }
110}
111
112/// Parse a `--preserve[=LIST]` attribute list.
113///
114/// Supports: mode, ownership, timestamps, links, context, xattr, all.
115pub fn apply_preserve(list: &str, config: &mut CpConfig) {
116    for attr in list.split(',') {
117        match attr.trim() {
118            "mode" => config.preserve_mode = true,
119            "ownership" => config.preserve_ownership = true,
120            "timestamps" => config.preserve_timestamps = true,
121            "links" | "context" | "xattr" => { /* acknowledged but not yet implemented */ }
122            "all" => {
123                config.preserve_mode = true;
124                config.preserve_ownership = true;
125                config.preserve_timestamps = true;
126            }
127            _ => {}
128        }
129    }
130}
131
132// ---- backup helpers ----
133
134/// Create a backup of `dst` if it exists, according to the configured backup mode.
135/// Returns `Ok(())` when no backup is needed or the backup was made successfully.
136fn make_backup(dst: &Path, config: &CpConfig) -> io::Result<()> {
137    let mode = match config.backup {
138        Some(m) => m,
139        None => return Ok(()),
140    };
141    if mode == BackupMode::None {
142        return Ok(());
143    }
144    if !dst.exists() {
145        return Ok(());
146    }
147
148    let backup_path = match mode {
149        BackupMode::Simple | BackupMode::None => {
150            let mut p = dst.as_os_str().to_os_string();
151            p.push(&config.suffix);
152            std::path::PathBuf::from(p)
153        }
154        BackupMode::Numbered => numbered_backup_path(dst),
155        BackupMode::Existing => {
156            // Use numbered if any numbered backup already exists.
157            let numbered = numbered_backup_candidate(dst, 1);
158            if numbered.exists() {
159                numbered_backup_path(dst)
160            } else {
161                let mut p = dst.as_os_str().to_os_string();
162                p.push(&config.suffix);
163                std::path::PathBuf::from(p)
164            }
165        }
166    };
167
168    std::fs::rename(dst, &backup_path)?;
169    Ok(())
170}
171
172fn numbered_backup_path(dst: &Path) -> std::path::PathBuf {
173    let mut n: u64 = 1;
174    loop {
175        let candidate = numbered_backup_candidate(dst, n);
176        if !candidate.exists() {
177            return candidate;
178        }
179        n += 1;
180    }
181}
182
183fn numbered_backup_candidate(dst: &Path, n: u64) -> std::path::PathBuf {
184    let mut p = dst.as_os_str().to_os_string();
185    p.push(format!(".~{}~", n));
186    std::path::PathBuf::from(p)
187}
188
189// ---- attribute preservation ----
190
191/// Preserve file attributes (mode, timestamps, ownership) from `src` on `dst`
192/// according to the configuration.
193fn preserve_attributes(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
194    let meta = std::fs::symlink_metadata(src)?;
195
196    #[cfg(unix)]
197    if config.preserve_mode {
198        let mode = meta.mode();
199        std::fs::set_permissions(dst, std::fs::Permissions::from_mode(mode))?;
200    }
201
202    #[cfg(unix)]
203    if config.preserve_timestamps {
204        let atime_spec = libc::timespec {
205            tv_sec: meta.atime(),
206            tv_nsec: meta.atime_nsec(),
207        };
208        let mtime_spec = libc::timespec {
209            tv_sec: meta.mtime(),
210            tv_nsec: meta.mtime_nsec(),
211        };
212        let times = [atime_spec, mtime_spec];
213        // SAFETY: CString::new checks for interior NULs; the path is valid UTF-8/bytes.
214        let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
215            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
216        // SAFETY: c_path is a valid NUL-terminated C string, times is a valid [timespec; 2].
217        let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
218        if ret != 0 {
219            return Err(io::Error::last_os_error());
220        }
221    }
222
223    #[cfg(unix)]
224    if config.preserve_ownership {
225        // SAFETY: CString::new checks for interior NULs; the path is valid bytes.
226        let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
227            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
228        // SAFETY: c_path is a valid NUL-terminated C string, uid/gid are valid u32 values.
229        let ret = unsafe { libc::lchown(c_path.as_ptr(), meta.uid(), meta.gid()) };
230        if ret != 0 {
231            // Ownership preservation may fail for non-root; ignore EPERM.
232            let err = io::Error::last_os_error();
233            if err.raw_os_error() != Some(libc::EPERM) {
234                return Err(err);
235            }
236        }
237    }
238
239    // Suppress unused-variable warnings on non-unix platforms.
240    #[cfg(not(unix))]
241    {
242        let _ = (&meta, config);
243    }
244
245    Ok(())
246}
247
248// ---- Linux copy_file_range optimisation ----
249
250#[cfg(target_os = "linux")]
251fn copy_file_range_linux(src: &Path, dst: &Path) -> io::Result<()> {
252    use std::os::unix::io::AsRawFd;
253
254    let src_file = std::fs::File::open(src)?;
255    let src_meta = src_file.metadata()?;
256    let len = src_meta.len();
257
258    let dst_file = std::fs::OpenOptions::new()
259        .write(true)
260        .create(true)
261        .truncate(true)
262        .open(dst)?;
263
264    let mut remaining = len as i64;
265    while remaining > 0 {
266        // Cap to isize::MAX to avoid overflow on 32-bit when casting to usize.
267        let to_copy = (remaining as u64).min(isize::MAX as u64) as usize;
268        // SAFETY: src_file and dst_file are valid open file descriptors;
269        // null offsets mean the kernel uses and updates the file offsets.
270        // Uses raw syscall instead of libc::copy_file_range to support
271        // older glibc versions (e.g. cross-compilation with cross-rs).
272        let ret = unsafe {
273            libc::syscall(
274                libc::SYS_copy_file_range,
275                src_file.as_raw_fd(),
276                std::ptr::null_mut::<libc::off64_t>(),
277                dst_file.as_raw_fd(),
278                std::ptr::null_mut::<libc::off64_t>(),
279                to_copy,
280                0u32,
281            )
282        };
283        if ret < 0 {
284            return Err(io::Error::last_os_error());
285        }
286        if ret == 0 {
287            // EOF before all bytes copied — break to avoid infinite loop
288            break;
289        }
290        remaining -= ret as i64;
291    }
292    Ok(())
293}
294
295// ---- single-file copy ----
296
297/// Copy a single file (or symlink) from `src` to `dst`.
298pub fn copy_file(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
299    let src_meta = if config.dereference == DerefMode::Always {
300        std::fs::metadata(src)?
301    } else {
302        std::fs::symlink_metadata(src)?
303    };
304
305    // Handle symlink when not dereferencing.
306    if src_meta.file_type().is_symlink() && config.dereference == DerefMode::Never {
307        let target = std::fs::read_link(src)?;
308        #[cfg(unix)]
309        {
310            std::os::unix::fs::symlink(&target, dst)?;
311        }
312        #[cfg(not(unix))]
313        {
314            // Fallback: try a regular copy (symlinks are not portable).
315            let _ = target;
316            std::fs::copy(src, dst)?;
317        }
318        return Ok(());
319    }
320
321    // Hard link mode.
322    if config.link {
323        std::fs::hard_link(src, dst)?;
324        return Ok(());
325    }
326
327    // Symbolic link mode.
328    if config.symbolic_link {
329        #[cfg(unix)]
330        {
331            std::os::unix::fs::symlink(src, dst)?;
332        }
333        #[cfg(not(unix))]
334        {
335            return Err(io::Error::new(
336                io::ErrorKind::Unsupported,
337                "symbolic links are not supported on this platform",
338            ));
339        }
340        return Ok(());
341    }
342
343    // Try reflink (FICLONE ioctl) for instant CoW copy on btrfs/XFS.
344    #[cfg(target_os = "linux")]
345    {
346        if matches!(config.reflink, ReflinkMode::Auto | ReflinkMode::Always) {
347            use std::os::unix::io::AsRawFd;
348            // FICLONE = _IOW(0x94, 9, int) from linux/fs.h
349            const FICLONE: libc::c_ulong = 0x40049409;
350
351            if let Ok(src_file) = std::fs::File::open(src) {
352                let dst_file = std::fs::OpenOptions::new()
353                    .write(true)
354                    .create(true)
355                    .truncate(true)
356                    .open(dst);
357                if let Ok(dst_file) = dst_file {
358                    // SAFETY: Both file descriptors are valid (files are open),
359                    // FICLONE takes an fd as argument, and we check the return value.
360                    let ret =
361                        unsafe { libc::ioctl(dst_file.as_raw_fd(), FICLONE, src_file.as_raw_fd()) };
362                    if ret == 0 {
363                        preserve_attributes(src, dst, config)?;
364                        return Ok(());
365                    }
366                    if config.reflink == ReflinkMode::Always {
367                        return Err(io::Error::new(
368                            io::ErrorKind::Unsupported,
369                            format!(
370                                "failed to clone '{}' to '{}': {}",
371                                src.display(),
372                                dst.display(),
373                                io::Error::last_os_error()
374                            ),
375                        ));
376                    }
377                    // Auto mode: fall through to other copy methods
378                }
379            }
380        }
381    }
382
383    // Try Linux copy_file_range for zero-copy.
384    #[cfg(target_os = "linux")]
385    {
386        match copy_file_range_linux(src, dst) {
387            Ok(()) => {
388                preserve_attributes(src, dst, config)?;
389                return Ok(());
390            }
391            Err(e)
392                if matches!(
393                    e.raw_os_error(),
394                    Some(libc::EINVAL | libc::ENOSYS | libc::EXDEV)
395                ) =>
396            {
397                // Unsupported/cross-device — fall through to std::fs::copy
398            }
399            Err(e) => return Err(e),
400        }
401    }
402
403    // Fallback: standard copy.
404    std::fs::copy(src, dst)?;
405    preserve_attributes(src, dst, config)?;
406    Ok(())
407}
408
409// ---- recursive copy ----
410
411/// Recursively copy `src` to `dst`.
412fn copy_recursive(
413    src: &Path,
414    dst: &Path,
415    config: &CpConfig,
416    root_dev: Option<u64>,
417) -> io::Result<()> {
418    let src_meta = std::fs::symlink_metadata(src)?;
419
420    #[cfg(unix)]
421    if config.one_file_system {
422        if let Some(dev) = root_dev {
423            if src_meta.dev() != dev {
424                return Ok(());
425            }
426        }
427    }
428
429    if src_meta.is_dir() {
430        if !dst.exists() {
431            std::fs::create_dir_all(dst)?;
432        }
433        for entry in std::fs::read_dir(src)? {
434            let entry = entry?;
435            let child_dst = dst.join(entry.file_name());
436            #[cfg(unix)]
437            let next_dev = Some(root_dev.unwrap_or(src_meta.dev()));
438            #[cfg(not(unix))]
439            let next_dev: Option<u64> = None;
440            copy_recursive(&entry.path(), &child_dst, config, next_dev)?;
441        }
442        // Preserve directory attributes after copying contents.
443        preserve_attributes(src, dst, config)?;
444    } else {
445        // If parent directory does not exist, create it.
446        if let Some(parent) = dst.parent() {
447            if !parent.exists() {
448                std::fs::create_dir_all(parent)?;
449            }
450        }
451        copy_file(src, dst, config)?;
452    }
453    Ok(())
454}
455
456// ---- main entry point ----
457
458/// Determine the effective destination and perform the copy.
459///
460/// `sources` is the list of source paths; `raw_dest` is the positional destination
461/// (may be `None` when `--target-directory` is used).
462///
463/// Returns a list of per-file error messages (empty on full success) and a bool
464/// indicating whether any error occurred.
465pub fn run_cp(
466    sources: &[String],
467    raw_dest: Option<&str>,
468    config: &CpConfig,
469) -> (Vec<String>, bool) {
470    let mut errors: Vec<String> = Vec::new();
471    let mut had_error = false;
472
473    // Resolve destination directory.
474    let dest_dir: Option<std::path::PathBuf> = config
475        .target_directory
476        .as_deref()
477        .or(raw_dest)
478        .map(std::path::PathBuf::from);
479
480    let dest_dir = match dest_dir {
481        Some(d) => d,
482        None => {
483            errors.push("cp: missing destination operand".to_string());
484            return (errors, true);
485        }
486    };
487
488    // Multiple sources or target is an existing directory => copy into directory.
489    let copy_into_dir = sources.len() > 1 || dest_dir.is_dir() || config.target_directory.is_some();
490
491    // When -T is set, never treat destination as a directory.
492    let copy_into_dir = copy_into_dir && !config.no_target_directory;
493
494    for source in sources {
495        let src = Path::new(source);
496        let dst = if copy_into_dir {
497            let name = src.file_name().unwrap_or(src.as_ref());
498            dest_dir.join(name)
499        } else {
500            dest_dir.clone()
501        };
502
503        if let Err(e) = do_copy(src, &dst, config) {
504            let msg = format!(
505                "cp: cannot copy '{}' to '{}': {}",
506                src.display(),
507                dst.display(),
508                strip_os_error(&e)
509            );
510            errors.push(msg);
511            had_error = true;
512        } else if config.verbose {
513            // Verbose output goes to stderr to match GNU behavior when piped.
514            eprintln!("'{}' -> '{}'", src.display(), dst.display());
515        }
516    }
517
518    (errors, had_error)
519}
520
521/// Core copy dispatcher for a single source -> destination pair.
522fn do_copy(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
523    let src_meta = if config.dereference == DerefMode::Always {
524        std::fs::metadata(src)?
525    } else {
526        std::fs::symlink_metadata(src)?
527    };
528
529    // Reject directory source without -R.
530    if src_meta.is_dir() && !config.recursive {
531        return Err(io::Error::new(
532            io::ErrorKind::Other,
533            format!("omitting directory '{}'", src.display()),
534        ));
535    }
536
537    // No-clobber: skip if destination exists.
538    if config.no_clobber && dst.exists() {
539        return Ok(());
540    }
541
542    // Update: skip if destination is same age or newer.
543    if config.update && dst.exists() {
544        if let (Ok(src_m), Ok(dst_m)) = (src.metadata(), dst.metadata()) {
545            if let (Ok(src_t), Ok(dst_t)) = (src_m.modified(), dst_m.modified()) {
546                if dst_t >= src_t {
547                    return Ok(());
548                }
549            }
550        }
551    }
552
553    // Interactive: prompt on stderr.
554    if config.interactive && dst.exists() {
555        eprint!("cp: overwrite '{}'? ", dst.display());
556        let mut response = String::new();
557        io::stdin().read_line(&mut response)?;
558        let r = response.trim().to_lowercase();
559        if !(r == "y" || r == "yes") {
560            return Ok(());
561        }
562    }
563
564    // Force: remove existing destination if it cannot be opened for writing.
565    if config.force && dst.exists() {
566        if let Ok(m) = dst.metadata() {
567            if m.permissions().readonly() {
568                std::fs::remove_file(dst)?;
569            }
570        }
571    }
572
573    // Make backup if requested.
574    make_backup(dst, config)?;
575
576    if src_meta.is_dir() {
577        #[cfg(unix)]
578        let root_dev = Some(src_meta.dev());
579        #[cfg(not(unix))]
580        let root_dev: Option<u64> = None;
581        copy_recursive(src, dst, config, root_dev)
582    } else {
583        copy_file(src, dst, config)
584    }
585}
586
587/// Strip the " (os error N)" suffix from an io::Error for GNU-compatible messages.
588fn strip_os_error(e: &io::Error) -> String {
589    if let Some(raw) = e.raw_os_error() {
590        let msg = format!("{}", e);
591        msg.replace(&format!(" (os error {})", raw), "")
592    } else {
593        format!("{}", e)
594    }
595}