use crate::atomic::{atomic_write_with_options, AtomicWriteOptions, Privacy, ReplaceMode};
use crate::error::{FsError, Result as FsResult};
use std::fs::{File, OpenOptions};
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;
if dst
.file_name()
.is_some_and(|n| crate::atomic::reject_reserved_name(&n.to_string_lossy()).is_err())
{
return Err(CloneError::InvalidPath(dst.to_path_buf()));
}
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()),
),
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CloneOutcome {
Cloned,
HardLinked,
Copied,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CloneOptions {
pub allow_hard_link: bool,
pub durable: bool,
pub fresh_metadata: bool,
}
impl Default for CloneOptions {
fn default() -> Self {
Self {
allow_hard_link: true,
durable: true,
fresh_metadata: false,
}
}
}
impl CloneOptions {
pub fn without_hard_link(self) -> Self {
Self {
allow_hard_link: false,
..self
}
}
pub fn fresh_metadata(self, fresh: bool) -> Self {
Self {
fresh_metadata: fresh,
..self
}
}
}
pub fn clone_or_link_or_copy_nofollow(src: &Path, dst: &Path) -> FsResult<CloneOutcome> {
clone_or_link_or_copy_nofollow_with(src, dst, &CloneOptions::default())
}
pub fn clone_or_link_or_copy_nofollow_with(
src: &Path,
dst: &Path,
options: &CloneOptions,
) -> FsResult<CloneOutcome> {
if let Some(name) = dst.file_name() {
crate::atomic::reject_reserved_name(&name.to_string_lossy())?;
}
check_source(src)?;
check_destination(dst)?;
if options.fresh_metadata {
copy_step(src, dst, options)?;
return Ok(CloneOutcome::Copied);
}
match run_step(Step::Clone, src, dst, options) {
StepResult::Done => return finish(dst, options, CloneOutcome::Cloned),
StepResult::Exists => return Err(FsError::AlreadyExists(dst.to_path_buf())),
StepResult::Failed(error) => return Err(error),
StepResult::Unavailable => {}
}
if options.allow_hard_link {
match run_step(Step::Link, src, dst, options) {
StepResult::Done => return finish(dst, options, CloneOutcome::HardLinked),
StepResult::Exists => return Err(FsError::AlreadyExists(dst.to_path_buf())),
StepResult::Failed(error) => return Err(error),
StepResult::Unavailable => {}
}
}
copy_step(src, dst, options)?;
Ok(CloneOutcome::Copied)
}
#[derive(Clone, Copy)]
enum Step {
Clone,
Link,
}
enum StepResult {
Done,
Exists,
Unavailable,
#[allow(dead_code)]
Failed(FsError),
}
fn run_step(step: Step, src: &Path, dst: &Path, options: &CloneOptions) -> StepResult {
match step {
Step::Clone => clone_step(src, dst, options),
Step::Link => match std::fs::hard_link(src, dst) {
Ok(()) => StepResult::Done,
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => StepResult::Exists,
Err(_) => StepResult::Unavailable,
},
}
}
#[cfg(target_os = "macos")]
fn source_must_not_be_cloned(src: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
match std::fs::symlink_metadata(src) {
Ok(meta) => meta.permissions().mode() & 0o077 == 0,
Err(_) => true,
}
}
#[cfg(target_os = "macos")]
fn clone_step(src: &Path, dst: &Path, _options: &CloneOptions) -> StepResult {
if source_must_not_be_cloned(src) {
return StepResult::Unavailable;
}
match clone_file_nofollow(src, dst) {
Ok(()) => StepResult::Done,
Err(CloneError::Io { source, .. }) if source.kind() == io::ErrorKind::AlreadyExists => {
StepResult::Exists
}
Err(_) => StepResult::Unavailable,
}
}
#[cfg(all(
target_os = "linux",
any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
)
))]
fn clone_step(src: &Path, dst: &Path, options: &CloneOptions) -> StepResult {
use crate::atomic::AtomicWriter;
use std::os::fd::AsRawFd;
const FICLONE: libc::c_ulong = 0x4004_9409;
let Ok(input) = open_source(src) else {
return StepResult::Unavailable;
};
let write_options = AtomicWriteOptions {
replace: ReplaceMode::NoReplace,
durable: options.durable,
privacy: Privacy::OwnerOnlyUnixInheritWindows,
..AtomicWriteOptions::default()
};
let mut writer = match AtomicWriter::create_with(dst, write_options) {
Ok(writer) => writer,
Err(_) => return StepResult::Unavailable,
};
let rc = unsafe {
libc::ioctl(
writer.file_mut().as_raw_fd(),
FICLONE as _,
input.as_raw_fd(),
)
};
if rc != 0 {
return StepResult::Unavailable;
}
let moded = input
.metadata()
.and_then(|meta| writer.file_mut().set_permissions(meta.permissions()));
if moded.is_err() {
return StepResult::Unavailable;
}
match writer.commit() {
Ok(()) => StepResult::Done,
Err(FsError::AlreadyExists(_)) => StepResult::Exists,
Err(e) if e.target_replaced() => StepResult::Failed(e),
Err(_) => StepResult::Unavailable,
}
}
#[cfg(not(any(
target_os = "macos",
all(
target_os = "linux",
any(
target_arch = "x86",
target_arch = "x86_64",
target_arch = "arm",
target_arch = "aarch64",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "s390x"
)
)
)))]
fn clone_step(_src: &Path, _dst: &Path, _options: &CloneOptions) -> StepResult {
StepResult::Unavailable
}
fn finish(dst: &Path, options: &CloneOptions, outcome: CloneOutcome) -> FsResult<CloneOutcome> {
if options.durable {
crate::atomic::sync_parent_dir(dst).map_err(|source| FsError::DirSyncFailed {
path: dst.to_path_buf(),
source,
})?;
}
Ok(outcome)
}
fn copy_step(src: &Path, dst: &Path, options: &CloneOptions) -> FsResult<()> {
let mut input = open_source(src).map_err(|e| match e.raw_os_error() {
#[cfg(unix)]
Some(libc::ELOOP) => FsError::Symlink(src.to_path_buf()),
_ => FsError::io(src, e),
})?;
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
let opened = input.metadata().map_err(|e| FsError::io(src, e))?;
if opened.file_attributes() & REPARSE_POINT != 0 {
return Err(FsError::Symlink(src.to_path_buf()));
}
}
let opened = input.metadata().map_err(|e| FsError::io(src, e))?;
if !opened.is_file() {
return Err(FsError::NotAFile(src.to_path_buf()));
}
let write_options = AtomicWriteOptions {
replace: ReplaceMode::NoReplace,
durable: options.durable,
privacy: if options.fresh_metadata {
Privacy::Inherit
} else {
Privacy::OwnerOnlyUnixInheritWindows
},
..AtomicWriteOptions::default()
};
atomic_write_with_options(dst, write_options, |out| {
io::copy(&mut input, out)?;
#[cfg(unix)]
if !options.fresh_metadata {
out.set_permissions(opened.permissions())?;
}
#[cfg(not(unix))]
let _ = (&opened, options.fresh_metadata);
Ok(())
})
}
#[cfg(windows)]
const REPARSE_POINT: u32 = 0x400;
fn open_source(src: &Path) -> io::Result<File> {
let mut open = OpenOptions::new();
open.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
open.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
open.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
open.open(src)
}
fn check_source(src: &Path) -> FsResult<std::fs::Metadata> {
let meta = std::fs::symlink_metadata(src).map_err(|e| FsError::io(src, e))?;
if is_link(&meta) {
return Err(FsError::Symlink(src.to_path_buf()));
}
if !meta.is_file() {
return Err(FsError::NotAFile(src.to_path_buf()));
}
Ok(meta)
}
fn check_destination(dst: &Path) -> FsResult<()> {
match std::fs::symlink_metadata(dst) {
Ok(meta) if is_link(&meta) => Err(FsError::Symlink(dst.to_path_buf())),
Ok(_) => Err(FsError::AlreadyExists(dst.to_path_buf())),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(FsError::io(dst, e)),
}
}
fn is_link(meta: &std::fs::Metadata) -> bool {
if meta.file_type().is_symlink() {
return true;
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
if meta.file_attributes() & REPARSE_POINT != 0 {
return true;
}
}
false
}