use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::AsRawFd;
use std::path::Path;
use anyhow::{Context, Result};
const FICLONE_IOCTL: libc::c_ulong = 0x4004_9409;
pub(crate) fn ficlone(dest: &File, src: &File) -> io::Result<()> {
let rc = unsafe { libc::ioctl(dest.as_raw_fd(), FICLONE_IOCTL, src.as_raw_fd()) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn is_fallback_errno(errno: i32) -> bool {
matches!(errno, libc::EXDEV | libc::EOPNOTSUPP | libc::ENOTTY)
}
pub(crate) fn reflink_or_copy(src: &Path, dest: &Path) -> Result<u64> {
let src_file = File::open(src).with_context(|| format!("open reflink source {src:?}"))?;
let dest_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(dest)
.with_context(|| format!("create reflink dest {dest:?}"))?;
match ficlone(&dest_file, &src_file) {
Ok(()) => {
let perms = src_file
.metadata()
.with_context(|| format!("stat reflink source {src:?}"))?
.permissions();
std::fs::set_permissions(dest, perms)
.with_context(|| format!("set reflink dest perms {dest:?}"))?;
let len = dest_file
.metadata()
.with_context(|| format!("stat reflink dest {dest:?}"))?
.len();
Ok(len)
}
Err(e) if e.raw_os_error().is_some_and(is_fallback_errno) => {
drop(dest_file);
match std::fs::copy(src, dest) {
Ok(n) => Ok(n),
Err(copy_err) => {
let _ = std::fs::remove_file(dest);
Err(copy_err).with_context(|| {
format!("copy {src:?} -> {dest:?} (filesystem has no reflink)")
})
}
}
}
Err(e) => {
drop(dest_file);
let _ = std::fs::remove_file(dest);
Err(e).with_context(|| {
format!(
"FICLONE {src:?} -> {dest:?} failed and the errno is not a \
reflink-unsupported signal"
)
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn reflink_or_copy_matches_content_and_perms_either_path() {
let base = std::env::temp_dir().join(format!("ktstr-reflink-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).expect("mk base");
let src = base.join("src.bin");
let dest = base.join("dest.bin");
std::fs::write(&src, b"reflink-or-copy payload\n").expect("write src");
std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o640)).expect("chmod src");
let n = reflink_or_copy(&src, &dest).expect("reflink_or_copy");
assert_eq!(
n,
std::fs::metadata(&src).unwrap().len(),
"returns the byte count",
);
assert_eq!(
std::fs::read(&dest).unwrap(),
std::fs::read(&src).unwrap(),
"dest content matches src (reflink or copy fallback)",
);
assert_eq!(
std::fs::metadata(&dest).unwrap().permissions().mode() & 0o777,
0o640,
"dest mode matches src on both the reflink and copy paths",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn reflink_or_copy_propagates_a_real_error() {
let base = std::env::temp_dir().join(format!("ktstr-reflink-err-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).expect("mk base");
let src = base.join("src.bin");
std::fs::write(&src, b"x").expect("write src");
let dest = base.join("nonexistent-parent").join("dest.bin");
assert!(
reflink_or_copy(&src, &dest).is_err(),
"a dest under a nonexistent parent must error, not silently copy",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn reflink_or_copy_leaves_no_dest_on_a_genuine_error() {
let base = std::env::temp_dir().join(format!("ktstr-reflink-dir-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).expect("mk base");
let src_dir = base.join("src-is-a-directory");
std::fs::create_dir(&src_dir).expect("mk src dir");
let dest = base.join("dest.bin");
assert!(
reflink_or_copy(&src_dir, &dest).is_err(),
"a directory source is a genuine error (neither reflink nor copy can clone a dir)",
);
assert!(
!dest.exists(),
"reflink_or_copy leaves no dest behind on a genuine error (both the \
non-fallback-errno arm and the fallback-copy-failure path clean up)",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn is_fallback_errno_whitelists_only_unsupported() {
assert!(is_fallback_errno(libc::EXDEV));
assert!(is_fallback_errno(libc::EOPNOTSUPP));
assert!(is_fallback_errno(libc::ENOTTY));
for genuine in [
libc::EBADF,
libc::EINVAL,
libc::EISDIR,
libc::EPERM,
libc::ENOSPC,
0,
] {
assert!(
!is_fallback_errno(genuine),
"errno {genuine} must propagate, not fall back",
);
}
}
}