use std::{fs, io, path::Path};
pub fn try_reflink(source: &Path, dest: &Path) -> io::Result<bool> {
#[cfg(target_os = "macos")]
{
try_clonefile_macos(source, dest)
}
#[cfg(target_os = "linux")]
{
try_ficlone_linux(source, dest)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
let _ = (source, dest);
Ok(false)
}
}
pub fn clonefile_or_copy(source: &Path, dest: &Path) -> io::Result<bool> {
let _ = fs::remove_file(dest);
if try_reflink(source, dest)? {
return Ok(true);
}
fs::copy(source, dest)?;
Ok(false)
}
#[cfg(target_os = "macos")]
fn try_clonefile_macos(source: &Path, dest: &Path) -> io::Result<bool> {
use std::{ffi::CString, os::unix::ffi::OsStrExt};
unsafe extern "C" {
fn clonefile(src: *const libc::c_char, dst: *const libc::c_char, flags: u32)
-> libc::c_int;
}
let src_c = CString::new(source.as_os_str().as_bytes()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"source path contains interior NUL",
)
})?;
let dst_c = CString::new(dest.as_os_str().as_bytes()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"destination path contains interior NUL",
)
})?;
let rc = unsafe { clonefile(src_c.as_ptr(), dst_c.as_ptr(), 0) };
if rc == 0 {
return Ok(true);
}
let err = io::Error::last_os_error();
if reflink_unsupported(&err) {
Ok(false)
} else {
Err(err)
}
}
#[cfg(target_os = "linux")]
fn try_ficlone_linux(source: &Path, dest: &Path) -> io::Result<bool> {
use std::{fs::OpenOptions, os::unix::io::AsRawFd};
const FICLONE: libc::c_ulong = 0x4004_9409;
let src = OpenOptions::new().read(true).open(source)?;
let dst = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(dest)?;
let rc = unsafe { libc::ioctl(dst.as_raw_fd(), FICLONE, src.as_raw_fd()) };
if rc == 0 {
return Ok(true);
}
let err = io::Error::last_os_error();
drop(dst);
let _ = fs::remove_file(dest);
if reflink_unsupported(&err) {
Ok(false)
} else {
Err(err)
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn reflink_unsupported(err: &io::Error) -> bool {
let Some(code) = err.raw_os_error() else {
return false;
};
#[allow(unreachable_patterns)]
let is_unsupported = matches!(
code,
libc::EXDEV | libc::EOPNOTSUPP | libc::ENOTSUP | libc::ENOSYS | libc::EINVAL
);
is_unsupported
}
pub fn filesystem_supports_reflink(parent_dir: &Path) -> bool {
use std::io::Write;
let src = parent_dir.join(".heddle-reflink-probe-src");
let dst = parent_dir.join(".heddle-reflink-probe-dst");
let _ = fs::remove_file(&src);
let _ = fs::remove_file(&dst);
let mut f = match fs::File::create(&src) {
Ok(f) => f,
Err(_) => return false,
};
if f.write_all(b"reflink-probe").is_err() {
let _ = fs::remove_file(&src);
return false;
}
drop(f);
let supported = matches!(try_reflink(&src, &dst), Ok(true));
let _ = fs::remove_file(&src);
let _ = fs::remove_file(&dst);
supported
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn clonefile_or_copy_creates_destination_with_source_bytes() {
let temp = TempDir::new().unwrap();
let src = temp.path().join("src.txt");
let dst = temp.path().join("dst.txt");
fs::write(&src, b"hello reflink").unwrap();
let _ = clonefile_or_copy(&src, &dst).unwrap();
assert_eq!(fs::read(&dst).unwrap(), b"hello reflink");
}
#[test]
fn clonefile_or_copy_overwrites_existing_destination() {
let temp = TempDir::new().unwrap();
let src = temp.path().join("src.txt");
let dst = temp.path().join("dst.txt");
fs::write(&src, b"new content").unwrap();
fs::write(&dst, b"old content").unwrap();
let _ = clonefile_or_copy(&src, &dst).unwrap();
assert_eq!(fs::read(&dst).unwrap(), b"new content");
}
#[test]
fn writing_to_destination_does_not_mutate_source() {
let temp = TempDir::new().unwrap();
let src = temp.path().join("src.txt");
let dst = temp.path().join("dst.txt");
fs::write(&src, b"original source").unwrap();
let _ = clonefile_or_copy(&src, &dst).unwrap();
fs::write(&dst, b"mutated dest").unwrap();
assert_eq!(fs::read(&src).unwrap(), b"original source");
assert_eq!(fs::read(&dst).unwrap(), b"mutated dest");
}
#[cfg(unix)]
#[test]
fn successful_reflink_yields_distinct_inode() {
use std::os::unix::fs::MetadataExt;
let temp = TempDir::new().unwrap();
if !filesystem_supports_reflink(temp.path()) {
eprintln!(
"[skip] filesystem at {:?} does not support reflinks; cannot assert inode property",
temp.path()
);
return;
}
let src = temp.path().join("src.txt");
let dst = temp.path().join("dst.txt");
fs::write(&src, b"reflink inode test").unwrap();
let did_reflink = try_reflink(&src, &dst).unwrap();
assert!(did_reflink, "filesystem advertised reflink support");
let src_inode = fs::metadata(&src).unwrap().ino();
let dst_inode = fs::metadata(&dst).unwrap().ino();
assert_ne!(
src_inode, dst_inode,
"reflinked files must have distinct inodes (got {} for both)",
src_inode
);
}
}