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#[cfg(target_os = "linux")]
16static FICLONE_UNSUPPORTED: std::sync::atomic::AtomicBool =
17 std::sync::atomic::AtomicBool::new(false);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum DerefMode {
22 Never,
24 CommandLine,
26 Always,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum BackupMode {
33 Numbered,
35 Existing,
37 Simple,
39 None,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ReflinkMode {
46 Auto,
48 Always,
50 Never,
52}
53
54pub struct CpConfig {
56 pub recursive: bool,
57 pub force: bool,
58 pub interactive: bool,
59 pub no_clobber: bool,
60 pub verbose: bool,
61 pub preserve_mode: bool,
62 pub preserve_ownership: bool,
63 pub preserve_timestamps: bool,
64 pub dereference: DerefMode,
65 pub link: bool,
66 pub symbolic_link: bool,
67 pub update: bool,
68 pub one_file_system: bool,
69 pub backup: Option<BackupMode>,
70 pub suffix: String,
71 pub reflink: ReflinkMode,
72 pub target_directory: Option<String>,
73 pub no_target_directory: bool,
74}
75
76impl Default for CpConfig {
77 fn default() -> Self {
78 Self {
79 recursive: false,
80 force: false,
81 interactive: false,
82 no_clobber: false,
83 verbose: false,
84 preserve_mode: false,
85 preserve_ownership: false,
86 preserve_timestamps: false,
87 dereference: DerefMode::CommandLine,
88 link: false,
89 symbolic_link: false,
90 update: false,
91 one_file_system: false,
92 backup: None,
93 suffix: "~".to_string(),
94 reflink: ReflinkMode::Auto,
95 target_directory: None,
96 no_target_directory: false,
97 }
98 }
99}
100
101pub fn parse_backup_mode(s: &str) -> Result<BackupMode, String> {
103 match s {
104 "none" | "off" => Ok(BackupMode::None),
105 "numbered" | "t" => Ok(BackupMode::Numbered),
106 "existing" | "nil" => Ok(BackupMode::Existing),
107 "simple" | "never" => Ok(BackupMode::Simple),
108 _ => Err(format!("invalid backup type '{}'", s)),
109 }
110}
111
112pub fn parse_reflink_mode(s: &str) -> Result<ReflinkMode, String> {
114 match s {
115 "auto" => Ok(ReflinkMode::Auto),
116 "always" => Ok(ReflinkMode::Always),
117 "never" => Ok(ReflinkMode::Never),
118 _ => Err(format!("invalid reflink value '{}'", s)),
119 }
120}
121
122pub fn apply_preserve(list: &str, config: &mut CpConfig) {
126 for attr in list.split(',') {
127 match attr.trim() {
128 "mode" => config.preserve_mode = true,
129 "ownership" => config.preserve_ownership = true,
130 "timestamps" => config.preserve_timestamps = true,
131 "links" | "context" | "xattr" => { }
132 "all" => {
133 config.preserve_mode = true;
134 config.preserve_ownership = true;
135 config.preserve_timestamps = true;
136 }
137 _ => {}
138 }
139 }
140}
141
142fn make_backup(dst: &Path, config: &CpConfig) -> io::Result<()> {
147 let mode = match config.backup {
148 Some(m) => m,
149 None => return Ok(()),
150 };
151 if mode == BackupMode::None {
152 return Ok(());
153 }
154 if !dst.exists() {
155 return Ok(());
156 }
157
158 let backup_path = match mode {
159 BackupMode::Simple | BackupMode::None => {
160 let mut p = dst.as_os_str().to_os_string();
161 p.push(&config.suffix);
162 std::path::PathBuf::from(p)
163 }
164 BackupMode::Numbered => numbered_backup_path(dst),
165 BackupMode::Existing => {
166 let numbered = numbered_backup_candidate(dst, 1);
168 if numbered.exists() {
169 numbered_backup_path(dst)
170 } else {
171 let mut p = dst.as_os_str().to_os_string();
172 p.push(&config.suffix);
173 std::path::PathBuf::from(p)
174 }
175 }
176 };
177
178 std::fs::rename(dst, &backup_path)?;
179 Ok(())
180}
181
182fn numbered_backup_path(dst: &Path) -> std::path::PathBuf {
183 let mut n: u64 = 1;
184 loop {
185 let candidate = numbered_backup_candidate(dst, n);
186 if !candidate.exists() {
187 return candidate;
188 }
189 n += 1;
190 }
191}
192
193fn numbered_backup_candidate(dst: &Path, n: u64) -> std::path::PathBuf {
194 let mut p = dst.as_os_str().to_os_string();
195 p.push(format!(".~{}~", n));
196 std::path::PathBuf::from(p)
197}
198
199fn preserve_attributes_from_meta(
204 meta: &std::fs::Metadata,
205 dst: &Path,
206 config: &CpConfig,
207) -> io::Result<()> {
208 #[cfg(unix)]
211 if config.preserve_mode {
212 let mode = meta.mode();
213 std::fs::set_permissions(dst, std::fs::Permissions::from_mode(mode))?;
214 }
215
216 #[cfg(unix)]
217 if config.preserve_timestamps {
218 let atime_spec = libc::timespec {
219 tv_sec: meta.atime(),
220 tv_nsec: meta.atime_nsec(),
221 };
222 let mtime_spec = libc::timespec {
223 tv_sec: meta.mtime(),
224 tv_nsec: meta.mtime_nsec(),
225 };
226 let times = [atime_spec, mtime_spec];
227 let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
229 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
230 let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
232 if ret != 0 {
233 return Err(io::Error::last_os_error());
234 }
235 }
236
237 #[cfg(unix)]
238 if config.preserve_ownership {
239 let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
241 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
242 let ret = unsafe { libc::lchown(c_path.as_ptr(), meta.uid(), meta.gid()) };
244 if ret != 0 {
245 let err = io::Error::last_os_error();
247 if err.raw_os_error() != Some(libc::EPERM) {
248 return Err(err);
249 }
250 }
251 }
252
253 #[cfg(not(unix))]
255 {
256 let _ = (meta, config);
257 }
258
259 Ok(())
260}
261
262#[cfg(not(target_os = "linux"))]
269fn copy_data_large_buf(src: &Path, dst: &Path, src_len: u64, src_mode: u32) -> io::Result<()> {
270 use std::cell::RefCell;
271 use std::io::{Read, Write};
272 const MAX_BUF: usize = 4 * 1024 * 1024; const SHRINK_THRESHOLD: usize = 512 * 1024; thread_local! {
278 static BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
279 }
280
281 let buf_size = src_len.min(MAX_BUF as u64).max(8192) as usize;
283
284 let mut reader = std::fs::File::open(src)?;
285 let mut opts = std::fs::OpenOptions::new();
286 opts.write(true).create(true).truncate(true);
287 #[cfg(unix)]
288 {
289 use std::os::unix::fs::OpenOptionsExt;
290 opts.mode(src_mode);
291 }
292 #[cfg(not(unix))]
293 let _ = src_mode;
294 let mut writer = opts.open(dst)?;
295
296 BUF.with(|cell| {
297 let mut buf = cell.borrow_mut();
298 if buf.len() > SHRINK_THRESHOLD && buf_size < buf.len() / 4 {
300 buf.resize(buf_size, 0);
301 buf.shrink_to_fit();
302 } else if buf.len() < buf_size {
303 buf.resize(buf_size, 0);
304 }
305 loop {
306 let n = reader.read(&mut buf[..buf_size])?;
307 if n == 0 {
308 break;
309 }
310 writer.write_all(&buf[..n])?;
311 }
312 Ok(())
313 })
314}
315
316#[cfg(target_os = "linux")]
323fn copy_data_linux(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
324 use std::os::unix::fs::OpenOptionsExt;
325 use std::os::unix::io::AsRawFd;
326
327 let src_file = std::fs::File::open(src)?;
328 let src_fd = src_file.as_raw_fd();
329
330 let fd_meta = src_file.metadata()?;
334 let len = fd_meta.len();
335
336 let dst_file = std::fs::OpenOptions::new()
337 .write(true)
338 .create(true)
339 .truncate(true)
340 .mode(fd_meta.mode())
341 .open(dst)?;
342 let dst_fd = dst_file.as_raw_fd();
343
344 unsafe {
347 let _ = libc::posix_fadvise(src_fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
348 }
349
350 if matches!(config.reflink, ReflinkMode::Auto | ReflinkMode::Always) {
352 const FICLONE: libc::c_ulong = 0x40049409;
353 let should_try = config.reflink == ReflinkMode::Always
354 || !FICLONE_UNSUPPORTED.load(std::sync::atomic::Ordering::Relaxed);
355
356 if should_try {
357 let ret = unsafe { libc::ioctl(dst_fd, FICLONE, src_fd) };
359 if ret == 0 {
360 return Ok(());
361 }
362 let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
363 if config.reflink == ReflinkMode::Always {
364 return Err(io::Error::new(
365 io::ErrorKind::Unsupported,
366 format!(
367 "failed to clone '{}' to '{}': {}",
368 src.display(),
369 dst.display(),
370 io::Error::from_raw_os_error(errno)
371 ),
372 ));
373 }
374 if matches!(errno, libc::EOPNOTSUPP | libc::ENOTTY | libc::ENOSYS) {
375 FICLONE_UNSUPPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
376 }
377 if errno == libc::EXDEV {
378 return readwrite_with_buffer(src_file, dst_file, len);
381 }
382 }
384 }
385
386 let mut remaining = match i64::try_from(len) {
388 Ok(v) => v,
389 Err(_) => return readwrite_with_buffer(src_file, dst_file, len),
391 };
392 let mut cfr_failed = false;
393 while remaining > 0 {
394 let to_copy = (remaining as u64).min(isize::MAX as u64) as usize;
395 let ret = unsafe {
398 libc::syscall(
399 libc::SYS_copy_file_range,
400 src_fd,
401 std::ptr::null_mut::<libc::off64_t>(),
402 dst_fd,
403 std::ptr::null_mut::<libc::off64_t>(),
404 to_copy,
405 0u32,
406 )
407 };
408 if ret < 0 {
409 let err = io::Error::last_os_error();
410 if matches!(
411 err.raw_os_error(),
412 Some(libc::EINVAL | libc::ENOSYS | libc::EXDEV)
413 ) {
414 cfr_failed = true;
415 break;
416 }
417 return Err(err);
418 }
419 if ret == 0 {
420 if remaining > 0 {
421 return Err(io::Error::new(
423 io::ErrorKind::UnexpectedEof,
424 "source file shrank during copy",
425 ));
426 }
427 break;
428 }
429 remaining -= ret as i64;
430 }
431 if !cfr_failed {
432 return Ok(());
433 }
434
435 use std::io::Seek;
438 let mut src_file = src_file;
439 let mut dst_file = dst_file;
440 src_file.seek(std::io::SeekFrom::Start(0))?;
441 dst_file.seek(std::io::SeekFrom::Start(0))?;
442 dst_file.set_len(0)?;
443
444 readwrite_with_buffer(src_file, dst_file, len)
445}
446
447#[cfg(target_os = "linux")]
449fn readwrite_with_buffer(
450 mut src_file: std::fs::File,
451 mut dst_file: std::fs::File,
452 len: u64,
453) -> io::Result<()> {
454 use std::cell::RefCell;
455 use std::io::{Read, Write};
456
457 const MAX_BUF: usize = 4 * 1024 * 1024;
458 const SHRINK_THRESHOLD: usize = 512 * 1024;
460
461 let buf_size = (len.min(MAX_BUF as u64) as usize).max(8192);
463
464 thread_local! {
465 static BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
466 }
467 BUF.with(|cell| {
468 let mut buf = cell.borrow_mut();
469 if buf.len() > SHRINK_THRESHOLD && buf_size < buf.len() / 4 {
470 buf.resize(buf_size, 0);
471 buf.shrink_to_fit();
472 } else if buf.len() < buf_size {
473 buf.resize(buf_size, 0);
474 }
475 loop {
476 let n = src_file.read(&mut buf[..buf_size])?;
477 if n == 0 {
478 break;
479 }
480 dst_file.write_all(&buf[..n])?;
481 }
482 Ok(())
483 })
484}
485
486pub fn copy_file(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
490 let src_meta = if config.dereference == DerefMode::Always {
491 std::fs::metadata(src)?
492 } else {
493 std::fs::symlink_metadata(src)?
494 };
495
496 copy_file_with_meta(src, dst, &src_meta, config)
497}
498
499fn copy_file_with_meta(
501 src: &Path,
502 dst: &Path,
503 src_meta: &std::fs::Metadata,
504 config: &CpConfig,
505) -> io::Result<()> {
506 if src_meta.file_type().is_symlink() && config.dereference == DerefMode::Never {
508 let target = std::fs::read_link(src)?;
509 #[cfg(unix)]
510 {
511 std::os::unix::fs::symlink(&target, dst)?;
512 }
513 #[cfg(not(unix))]
514 {
515 let _ = target;
517 std::fs::copy(src, dst)?;
518 }
519 return Ok(());
520 }
521
522 if config.link {
524 std::fs::hard_link(src, dst)?;
525 return Ok(());
526 }
527
528 if config.symbolic_link {
530 #[cfg(unix)]
531 {
532 std::os::unix::fs::symlink(src, dst)?;
533 }
534 #[cfg(not(unix))]
535 {
536 return Err(io::Error::new(
537 io::ErrorKind::Unsupported,
538 "symbolic links are not supported on this platform",
539 ));
540 }
541 return Ok(());
542 }
543
544 #[cfg(target_os = "linux")]
546 {
547 copy_data_linux(src, dst, config)?;
548 preserve_attributes_from_meta(src_meta, dst, config)?;
549 return Ok(());
550 }
551
552 #[cfg(not(target_os = "linux"))]
554 {
555 #[cfg(unix)]
556 let mode = src_meta.mode();
557 #[cfg(not(unix))]
558 let mode = 0o666u32;
559 copy_data_large_buf(src, dst, src_meta.len(), mode)?;
560 preserve_attributes_from_meta(src_meta, dst, config)?;
561 Ok(())
562 }
563}
564
565fn copy_recursive(
569 src: &Path,
570 dst: &Path,
571 config: &CpConfig,
572 root_dev: Option<u64>,
573) -> io::Result<()> {
574 let src_meta = std::fs::symlink_metadata(src)?;
575
576 #[cfg(unix)]
577 if config.one_file_system {
578 if let Some(dev) = root_dev {
579 if src_meta.dev() != dev {
580 return Ok(());
581 }
582 }
583 }
584
585 if src_meta.is_dir() {
586 if !dst.exists() {
587 std::fs::create_dir_all(dst)?;
588 }
589
590 #[cfg(unix)]
591 let next_dev = Some(root_dev.unwrap_or(src_meta.dev()));
592 #[cfg(not(unix))]
593 let next_dev: Option<u64> = None;
594
595 let mut files: Vec<(std::path::PathBuf, std::path::PathBuf, std::fs::Metadata)> =
597 Vec::new();
598 let mut dirs: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new();
599
600 for entry in std::fs::read_dir(src)? {
601 let entry = entry?;
602 let child_src = entry.path();
603 let child_dst = dst.join(entry.file_name());
604 let meta = if config.dereference == DerefMode::Always {
606 std::fs::metadata(&child_src)?
607 } else {
608 std::fs::symlink_metadata(&child_src)?
609 };
610 #[cfg(unix)]
612 if config.one_file_system {
613 if let Some(dev) = root_dev {
614 if meta.dev() != dev {
615 continue;
616 }
617 }
618 }
619 if meta.is_dir() {
620 dirs.push((child_src, child_dst));
621 } else {
622 files.push((child_src, child_dst, meta));
623 }
624 }
625
626 const PARALLEL_FILE_THRESHOLD: usize = 8;
629
630 if files.len() >= PARALLEL_FILE_THRESHOLD {
632 use rayon::prelude::*;
633 let result: Result<(), io::Error> =
634 files
635 .par_iter()
636 .try_for_each(|(child_src, child_dst, meta)| {
637 copy_file_with_meta(child_src, child_dst, meta, config)
638 });
639 result?;
640 } else {
641 for (child_src, child_dst, meta) in &files {
642 copy_file_with_meta(child_src, child_dst, meta, config)?;
643 }
644 }
645
646 for (child_src, child_dst) in &dirs {
649 copy_recursive(child_src, child_dst, config, next_dev)?;
650 }
651
652 preserve_attributes_from_meta(&src_meta, dst, config)?;
654 } else {
655 if let Some(parent) = dst.parent() {
657 if !parent.exists() {
658 std::fs::create_dir_all(parent)?;
659 }
660 }
661 copy_file_with_meta(src, dst, &src_meta, config)?;
662 }
663 Ok(())
664}
665
666pub fn run_cp(
676 sources: &[String],
677 raw_dest: Option<&str>,
678 config: &CpConfig,
679) -> (Vec<String>, bool) {
680 let mut errors: Vec<String> = Vec::new();
681 let mut had_error = false;
682
683 let dest_dir: Option<std::path::PathBuf> = config
685 .target_directory
686 .as_deref()
687 .or(raw_dest)
688 .map(std::path::PathBuf::from);
689
690 let dest_dir = match dest_dir {
691 Some(d) => d,
692 None => {
693 errors.push("cp: missing destination operand".to_string());
694 return (errors, true);
695 }
696 };
697
698 let copy_into_dir = sources.len() > 1 || dest_dir.is_dir() || config.target_directory.is_some();
700
701 let copy_into_dir = copy_into_dir && !config.no_target_directory;
703
704 for source in sources {
705 let src = Path::new(source);
706 let dst = if copy_into_dir {
707 let name = src.file_name().unwrap_or(src.as_ref());
708 dest_dir.join(name)
709 } else {
710 dest_dir.clone()
711 };
712
713 if let Err(e) = do_copy(src, &dst, config) {
714 let msg = format!(
715 "cp: cannot copy '{}' to '{}': {}",
716 src.display(),
717 dst.display(),
718 strip_os_error(&e)
719 );
720 errors.push(msg);
721 had_error = true;
722 } else if config.verbose {
723 eprintln!("'{}' -> '{}'", src.display(), dst.display());
725 }
726 }
727
728 (errors, had_error)
729}
730
731fn do_copy(src: &Path, dst: &Path, config: &CpConfig) -> io::Result<()> {
733 let src_meta = if config.dereference == DerefMode::Always {
734 std::fs::metadata(src)?
735 } else {
736 std::fs::symlink_metadata(src)?
737 };
738
739 if src_meta.is_dir() && !config.recursive {
741 return Err(io::Error::new(
742 io::ErrorKind::Other,
743 format!("omitting directory '{}'", src.display()),
744 ));
745 }
746
747 if config.no_clobber && dst.exists() {
749 return Ok(());
750 }
751
752 if config.update && dst.exists() {
754 if let (Ok(src_m), Ok(dst_m)) = (src.metadata(), dst.metadata()) {
755 if let (Ok(src_t), Ok(dst_t)) = (src_m.modified(), dst_m.modified()) {
756 if dst_t >= src_t {
757 return Ok(());
758 }
759 }
760 }
761 }
762
763 if config.interactive && dst.exists() {
765 eprint!("cp: overwrite '{}'? ", dst.display());
766 let mut response = String::new();
767 io::stdin().read_line(&mut response)?;
768 let r = response.trim().to_lowercase();
769 if !(r == "y" || r == "yes") {
770 return Ok(());
771 }
772 }
773
774 if config.force && dst.exists() {
776 if let Ok(m) = dst.metadata() {
777 if m.permissions().readonly() {
778 std::fs::remove_file(dst)?;
779 }
780 }
781 }
782
783 make_backup(dst, config)?;
785
786 if src_meta.is_dir() {
787 #[cfg(unix)]
788 let root_dev = Some(src_meta.dev());
789 #[cfg(not(unix))]
790 let root_dev: Option<u64> = None;
791 copy_recursive(src, dst, config, root_dev)
792 } else {
793 copy_file(src, dst, config)
794 }
795}
796
797fn strip_os_error(e: &io::Error) -> String {
799 if let Some(raw) = e.raw_os_error() {
800 let msg = format!("{}", e);
801 msg.replace(&format!(" (os error {})", raw), "")
802 } else {
803 format!("{}", e)
804 }
805}