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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DerefMode {
12 Never,
14 CommandLine,
16 Always,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum BackupMode {
23 Numbered,
25 Existing,
27 Simple,
29 None,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ReflinkMode {
36 Auto,
38 Always,
40 Never,
42}
43
44pub 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
91pub 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
102pub 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
112pub 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" => { }
122 "all" => {
123 config.preserve_mode = true;
124 config.preserve_ownership = true;
125 config.preserve_timestamps = true;
126 }
127 _ => {}
128 }
129 }
130}
131
132fn 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 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
189fn 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 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 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 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 let ret = unsafe { libc::lchown(c_path.as_ptr(), meta.uid(), meta.gid()) };
230 if ret != 0 {
231 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 #[cfg(not(unix))]
241 {
242 let _ = (&meta, config);
243 }
244
245 Ok(())
246}
247
248#[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 let to_copy = (remaining as u64).min(isize::MAX as u64) as usize;
268 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 break;
289 }
290 remaining -= ret as i64;
291 }
292 Ok(())
293}
294
295pub 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 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 let _ = target;
316 std::fs::copy(src, dst)?;
317 }
318 return Ok(());
319 }
320
321 if config.link {
323 std::fs::hard_link(src, dst)?;
324 return Ok(());
325 }
326
327 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 #[cfg(target_os = "linux")]
345 {
346 if matches!(config.reflink, ReflinkMode::Auto | ReflinkMode::Always) {
347 use std::os::unix::io::AsRawFd;
348 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 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 }
379 }
380 }
381 }
382
383 #[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 }
399 Err(e) => return Err(e),
400 }
401 }
402
403 std::fs::copy(src, dst)?;
405 preserve_attributes(src, dst, config)?;
406 Ok(())
407}
408
409fn 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_attributes(src, dst, config)?;
444 } else {
445 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
456pub 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 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 let copy_into_dir = sources.len() > 1 || dest_dir.is_dir() || config.target_directory.is_some();
490
491 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 eprintln!("'{}' -> '{}'", src.display(), dst.display());
515 }
516 }
517
518 (errors, had_error)
519}
520
521fn 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 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 if config.no_clobber && dst.exists() {
539 return Ok(());
540 }
541
542 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 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 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(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
587fn 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}