1use std::io;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, thiserror::Error)]
13pub enum CloneError {
14 #[error("{0} is a symbolic link; clone refuses links")]
16 Symlink(PathBuf),
17 #[error("{0} is not a regular file")]
19 NotAFile(PathBuf),
20 #[error("{src} and {dst} are on different volumes")]
22 CrossVolume { src: PathBuf, dst: PathBuf },
23 #[error("clonefile is not supported for {path}: {source}")]
25 Unsupported {
26 path: PathBuf,
27 #[source]
28 source: io::Error,
29 },
30 #[error("path contains a NUL byte: {0}")]
32 InvalidPath(PathBuf),
33 #[error("clone of {path} failed: {source}")]
35 Io {
36 path: PathBuf,
37 #[source]
38 source: io::Error,
39 },
40}
41
42impl CloneError {
43 pub fn code(&self) -> &'static str {
45 match self {
46 Self::Symlink(_) => "CLONE_SYMLINK_REFUSED",
47 Self::NotAFile(_) => "CLONE_NOT_A_FILE",
48 Self::CrossVolume { .. } => "CLONE_CROSS_VOLUME",
49 Self::Unsupported { .. } => "CLONE_UNSUPPORTED",
50 Self::InvalidPath(_) => "CLONE_INVALID_PATH",
51 Self::Io { .. } => "CLONE_IO",
52 }
53 }
54
55 pub fn exit_code(&self) -> u8 {
57 match self {
58 Self::Unsupported { .. } => 3,
59 Self::CrossVolume { .. } => 4,
60 Self::Symlink(_) => 5,
61 Self::NotAFile(_) => 6,
62 Self::InvalidPath(_) => 2,
63 Self::Io { .. } => 1,
64 }
65 }
66
67 #[cfg(target_os = "macos")]
68 fn io(path: &Path, source: io::Error) -> Self {
69 Self::Io {
70 path: path.to_path_buf(),
71 source,
72 }
73 }
74}
75
76#[cfg(target_os = "macos")]
81pub fn clone_file_nofollow(src: &Path, dst: &Path) -> Result<(), CloneError> {
82 use std::ffi::CString;
83 use std::os::unix::ffi::OsStrExt;
84
85 const CLONE_NOFOLLOW: u32 = 0x0001;
87
88 let meta = std::fs::symlink_metadata(src).map_err(|source| CloneError::io(src, source))?;
89 if meta.file_type().is_symlink() {
90 return Err(CloneError::Symlink(src.to_path_buf()));
91 }
92 if !meta.is_file() {
93 return Err(CloneError::NotAFile(src.to_path_buf()));
94 }
95 let c_src = CString::new(src.as_os_str().as_bytes())
96 .map_err(|_| CloneError::InvalidPath(src.to_path_buf()))?;
97 let c_dst = CString::new(dst.as_os_str().as_bytes())
98 .map_err(|_| CloneError::InvalidPath(dst.to_path_buf()))?;
99 let rc = unsafe { libc::clonefile(c_src.as_ptr(), c_dst.as_ptr(), CLONE_NOFOLLOW) };
101 if rc == 0 {
102 return Ok(());
103 }
104 let source = io::Error::last_os_error();
105 Err(match source.raw_os_error() {
106 Some(libc::EXDEV) => CloneError::CrossVolume {
107 src: src.to_path_buf(),
108 dst: dst.to_path_buf(),
109 },
110 Some(code) if code == libc::ENOTSUP || code == libc::ENOSYS => CloneError::Unsupported {
111 path: dst.to_path_buf(),
112 source,
113 },
114 _ => CloneError::io(dst, source),
115 })
116}
117
118#[cfg(not(target_os = "macos"))]
120pub fn clone_file_nofollow(src: &Path, dst: &Path) -> Result<(), CloneError> {
121 Err(CloneError::Unsupported {
122 path: dst.to_path_buf(),
123 source: io::Error::new(
124 io::ErrorKind::Unsupported,
125 format!("clonefile is macOS-only (source {})", src.display()),
126 ),
127 })
128}