use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum CloneError {
#[error("{0} is a symbolic link; clone refuses links")]
Symlink(PathBuf),
#[error("{0} is not a regular file")]
NotAFile(PathBuf),
#[error("{src} and {dst} are on different volumes")]
CrossVolume { src: PathBuf, dst: PathBuf },
#[error("clonefile is not supported for {path}: {source}")]
Unsupported {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("path contains a NUL byte: {0}")]
InvalidPath(PathBuf),
#[error("clone of {path} failed: {source}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
}
impl CloneError {
pub fn code(&self) -> &'static str {
match self {
Self::Symlink(_) => "CLONE_SYMLINK_REFUSED",
Self::NotAFile(_) => "CLONE_NOT_A_FILE",
Self::CrossVolume { .. } => "CLONE_CROSS_VOLUME",
Self::Unsupported { .. } => "CLONE_UNSUPPORTED",
Self::InvalidPath(_) => "CLONE_INVALID_PATH",
Self::Io { .. } => "CLONE_IO",
}
}
pub fn exit_code(&self) -> u8 {
match self {
Self::Unsupported { .. } => 3,
Self::CrossVolume { .. } => 4,
Self::Symlink(_) => 5,
Self::NotAFile(_) => 6,
Self::InvalidPath(_) => 2,
Self::Io { .. } => 1,
}
}
#[cfg(target_os = "macos")]
fn io(path: &Path, source: io::Error) -> Self {
Self::Io {
path: path.to_path_buf(),
source,
}
}
}
#[cfg(target_os = "macos")]
pub fn clone_file_nofollow(src: &Path, dst: &Path) -> Result<(), CloneError> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
const CLONE_NOFOLLOW: u32 = 0x0001;
let meta = std::fs::symlink_metadata(src).map_err(|source| CloneError::io(src, source))?;
if meta.file_type().is_symlink() {
return Err(CloneError::Symlink(src.to_path_buf()));
}
if !meta.is_file() {
return Err(CloneError::NotAFile(src.to_path_buf()));
}
let c_src = CString::new(src.as_os_str().as_bytes())
.map_err(|_| CloneError::InvalidPath(src.to_path_buf()))?;
let c_dst = CString::new(dst.as_os_str().as_bytes())
.map_err(|_| CloneError::InvalidPath(dst.to_path_buf()))?;
let rc = unsafe { libc::clonefile(c_src.as_ptr(), c_dst.as_ptr(), CLONE_NOFOLLOW) };
if rc == 0 {
return Ok(());
}
let source = io::Error::last_os_error();
Err(match source.raw_os_error() {
Some(libc::EXDEV) => CloneError::CrossVolume {
src: src.to_path_buf(),
dst: dst.to_path_buf(),
},
Some(code) if code == libc::ENOTSUP || code == libc::ENOSYS => CloneError::Unsupported {
path: dst.to_path_buf(),
source,
},
_ => CloneError::io(dst, source),
})
}
#[cfg(not(target_os = "macos"))]
pub fn clone_file_nofollow(src: &Path, dst: &Path) -> Result<(), CloneError> {
Err(CloneError::Unsupported {
path: dst.to_path_buf(),
source: io::Error::new(
io::ErrorKind::Unsupported,
format!("clonefile is macOS-only (source {})", src.display()),
),
})
}