use std::fs::{File, OpenOptions};
use std::io;
#[cfg(windows)]
use std::io::{Read, Seek, SeekFrom, Write};
#[cfg(unix)]
use std::os::unix::io::{AsRawFd, RawFd};
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
use std::path::Path;
#[cfg(windows)]
use std::ptr;
#[cfg(windows)]
use crate::extent::mark_sparse;
#[cfg(windows)]
use windows_sys::Win32::Foundation::HANDLE;
#[cfg(windows)]
use windows_sys::Win32::Storage::FileSystem::GetVolumeInformationByHandleW;
#[cfg(windows)]
use windows_sys::Win32::System::IO::DeviceIoControl;
#[cfg(windows)]
use windows_sys::Win32::System::Ioctl::{
DUPLICATE_EXTENTS_DATA, FSCTL_DUPLICATE_EXTENTS_TO_FILE, FSCTL_GET_INTEGRITY_INFORMATION,
FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, FSCTL_SET_INTEGRITY_INFORMATION,
FSCTL_SET_INTEGRITY_INFORMATION_BUFFER,
};
#[cfg(windows)]
use windows_sys::Win32::System::SystemServices::FILE_SUPPORTS_BLOCK_REFCOUNTING;
#[cfg(windows)]
const WINDOWS_CLONE_ALIGNMENT: u64 = 64 * 1024;
#[cfg(windows)]
const WINDOWS_MAX_CLONE_CHUNK: u64 =
(u32::MAX as u64 / WINDOWS_CLONE_ALIGNMENT) * WINDOWS_CLONE_ALIGNMENT;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FastCopyStrategy {
Reflink,
SparseCopy,
}
pub fn fast_copy(src: &Path, dst: &Path) -> io::Result<u64> {
fast_copy_with_strategy(src, dst).map(|(len, _)| len)
}
pub fn fast_copy_with_strategy(src: &Path, dst: &Path) -> io::Result<(u64, FastCopyStrategy)> {
let src_len = std::fs::metadata(src)?.len();
match reflink_impl(src, dst) {
Ok(()) => return Ok((src_len, FastCopyStrategy::Reflink)),
Err(e) if is_reflink_unsupported(&e) => {
}
Err(e) => return Err(e),
}
sparse_copy(src, dst).map(|len| (len, FastCopyStrategy::SparseCopy))
}
pub fn reflink(src: &Path, dst: &Path) -> io::Result<u64> {
let src_len = std::fs::metadata(src)?.len();
reflink_impl(src, dst)?;
Ok(src_len)
}
pub fn sparse_copy(src: &Path, dst: &Path) -> io::Result<u64> {
sparse_copy_impl(src, dst)
}
#[cfg(unix)]
fn sparse_copy_impl(src: &Path, dst: &Path) -> io::Result<u64> {
let src_file = File::open(src)?;
let len = src_file.metadata()?.len();
let dst_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(dst)?;
dst_file.set_len(len)?;
let src_fd = src_file.as_raw_fd();
let dst_fd = dst_file.as_raw_fd();
let mut off: i64 = 0;
while (off as u64) < len {
let data_start = unsafe { libc::lseek(src_fd, off, libc::SEEK_DATA) };
if data_start < 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENXIO) {
break;
}
return Err(err);
}
let data_end = unsafe { libc::lseek(src_fd, data_start, libc::SEEK_HOLE) };
if data_end < 0 {
return Err(io::Error::last_os_error());
}
let data_end = (data_end as u64).min(len);
let data_start = data_start as u64;
if data_end <= data_start {
break;
}
copy_extent(src_fd, dst_fd, data_start, data_end - data_start)?;
off = data_end as i64;
}
dst_file.sync_all()?;
Ok(len)
}
#[cfg(unix)]
fn reflink_impl(src: &Path, dst: &Path) -> io::Result<()> {
reflink_copy::reflink(src, dst)
}
#[cfg(windows)]
fn reflink_impl(src: &Path, dst: &Path) -> io::Result<()> {
let mut src_file = File::open(src)?;
let mut dst_file = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(dst)?;
let result = reflink_windows_files(&mut src_file, &mut dst_file);
drop(dst_file);
drop(src_file);
if result.is_err() {
let _ = std::fs::remove_file(dst);
}
result
}
#[cfg(not(any(unix, windows)))]
fn reflink_impl(_src: &Path, _dst: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"filesystem reflinks are unsupported on this platform",
))
}
#[cfg(windows)]
fn sparse_copy_impl(src: &Path, dst: &Path) -> io::Result<u64> {
const BUF_SIZE: usize = 1024 * 1024;
let mut src_file = File::open(src)?;
let len = src_file.metadata()?.len();
let mut dst_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(dst)?;
dst_file.set_len(len)?;
mark_sparse(&dst_file)?;
let mut offset = 0u64;
let mut buf = vec![0u8; BUF_SIZE];
loop {
let n = src_file.read(&mut buf)?;
if n == 0 {
break;
}
write_nonzero_runs(&mut dst_file, offset, &buf[..n])?;
offset += n as u64;
}
dst_file.sync_all()?;
Ok(len)
}
#[cfg(windows)]
fn reflink_windows_files(src: &mut File, dst: &mut File) -> io::Result<()> {
let src_volume = windows_volume_identity(src)?;
let dst_volume = windows_volume_identity(dst)?;
if src_volume.0 != dst_volume.0 {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"Windows block cloning requires source and destination on the same volume",
));
}
if src_volume.1 & FILE_SUPPORTS_BLOCK_REFCOUNTING == 0
|| dst_volume.1 & FILE_SUPPORTS_BLOCK_REFCOUNTING == 0
{
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"destination volume does not advertise block-refcounting support",
));
}
mark_sparse(dst)?;
match_windows_integrity(src, dst)?;
let len = src.metadata()?.len();
dst.set_len(len)?;
let clone_len = len / WINDOWS_CLONE_ALIGNMENT * WINDOWS_CLONE_ALIGNMENT;
let mut offset = 0u64;
while offset < clone_len {
let chunk = (clone_len - offset).min(WINDOWS_MAX_CLONE_CHUNK);
duplicate_windows_extents(src, dst, offset, chunk)?;
offset += chunk;
}
if clone_len < len {
copy_windows_tail(src, dst, clone_len, len - clone_len)?;
}
Ok(())
}
#[cfg(windows)]
fn windows_volume_identity(file: &File) -> io::Result<(u32, u32)> {
let mut serial = 0u32;
let mut flags = 0u32;
let ok = unsafe {
GetVolumeInformationByHandleW(
file.as_raw_handle() as HANDLE,
ptr::null_mut(),
0,
&mut serial,
ptr::null_mut(),
&mut flags,
ptr::null_mut(),
0,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok((serial, flags))
}
#[cfg(windows)]
fn match_windows_integrity(src: &File, dst: &File) -> io::Result<()> {
let Some(src_info) = get_windows_integrity(src)? else {
return Ok(());
};
let Some(dst_info) = get_windows_integrity(dst)? else {
return Ok(());
};
if src_info.ChecksumAlgorithm == dst_info.ChecksumAlgorithm && src_info.Flags == dst_info.Flags
{
return Ok(());
}
let info = FSCTL_SET_INTEGRITY_INFORMATION_BUFFER {
ChecksumAlgorithm: src_info.ChecksumAlgorithm,
Reserved: 0,
Flags: src_info.Flags,
};
let mut returned = 0u32;
let ok = unsafe {
DeviceIoControl(
dst.as_raw_handle() as HANDLE,
FSCTL_SET_INTEGRITY_INFORMATION,
&info as *const _ as *const _,
size_of::<FSCTL_SET_INTEGRITY_INFORMATION_BUFFER>() as u32,
ptr::null_mut(),
0,
&mut returned,
ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(windows)]
fn get_windows_integrity(
file: &File,
) -> io::Result<Option<FSCTL_GET_INTEGRITY_INFORMATION_BUFFER>> {
let mut info = FSCTL_GET_INTEGRITY_INFORMATION_BUFFER::default();
let mut returned = 0u32;
let ok = unsafe {
DeviceIoControl(
file.as_raw_handle() as HANDLE,
FSCTL_GET_INTEGRITY_INFORMATION,
ptr::null(),
0,
&mut info as *mut _ as *mut _,
size_of::<FSCTL_GET_INTEGRITY_INFORMATION_BUFFER>() as u32,
&mut returned,
ptr::null_mut(),
)
};
if ok != 0 {
return Ok(Some(info));
}
let error = io::Error::last_os_error();
if is_reflink_unsupported(&error) {
Ok(None)
} else {
Err(error)
}
}
#[cfg(windows)]
fn duplicate_windows_extents(src: &File, dst: &File, offset: u64, len: u64) -> io::Result<()> {
let request = DUPLICATE_EXTENTS_DATA {
FileHandle: src.as_raw_handle() as HANDLE,
SourceFileOffset: offset as i64,
TargetFileOffset: offset as i64,
ByteCount: len as i64,
};
let mut returned = 0u32;
let ok = unsafe {
DeviceIoControl(
dst.as_raw_handle() as HANDLE,
FSCTL_DUPLICATE_EXTENTS_TO_FILE,
&request as *const _ as *const _,
size_of::<DUPLICATE_EXTENTS_DATA>() as u32,
ptr::null_mut(),
0,
&mut returned,
ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(windows)]
fn copy_windows_tail(src: &mut File, dst: &mut File, offset: u64, len: u64) -> io::Result<()> {
src.seek(SeekFrom::Start(offset))?;
dst.seek(SeekFrom::Start(offset))?;
let copied = io::copy(&mut src.take(len), dst)?;
if copied != len {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Windows reflink tail copied {copied} of {len} bytes"),
));
}
Ok(())
}
fn is_reflink_unsupported(e: &io::Error) -> bool {
if matches!(e.kind(), io::ErrorKind::Unsupported) {
return true;
}
let Some(code) = e.raw_os_error() else {
return false;
};
#[cfg(target_os = "linux")]
let aliases: &[i32] = &[libc::ENOTSUP, libc::EXDEV, libc::EINVAL];
#[cfg(all(unix, not(target_os = "linux")))]
let aliases: &[i32] = &[libc::ENOTSUP, libc::EOPNOTSUPP, libc::EXDEV, libc::EINVAL];
#[cfg(windows)]
let aliases: &[i32] = &[
1, 17, 50, 87, 124, 775, ];
#[cfg(windows)]
{
let win32_code = (code as u32 & 0xffff) as i32;
aliases.contains(&code) || aliases.contains(&win32_code)
}
#[cfg(unix)]
aliases.contains(&code)
}
#[cfg(unix)]
fn copy_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
read_write_extent(src_fd, dst_fd, off, len)
}
#[cfg(unix)]
fn read_write_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
const BUF_SIZE: usize = 1024 * 1024;
let mut buf = vec![0u8; BUF_SIZE];
let mut copied: u64 = 0;
while copied < len {
let to_read = (len - copied).min(BUF_SIZE as u64) as usize;
let read_off = (off + copied) as i64;
let n = unsafe {
libc::pread(
src_fd,
buf.as_mut_ptr() as *mut libc::c_void,
to_read,
read_off,
)
};
if n < 0 {
return Err(io::Error::last_os_error());
}
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"unexpected EOF mid-extent",
));
}
let n = n as usize;
let mut written: usize = 0;
while written < n {
let w_off = (off + copied + written as u64) as i64;
let w = unsafe {
libc::pwrite(
dst_fd,
buf[written..n].as_ptr() as *const libc::c_void,
n - written,
w_off,
)
};
if w < 0 {
return Err(io::Error::last_os_error());
}
if w == 0 {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"pwrite returned 0",
));
}
written += w as usize;
}
copied += n as u64;
}
Ok(())
}
#[cfg(windows)]
fn write_nonzero_runs(dst: &mut File, base_offset: u64, bytes: &[u8]) -> io::Result<()> {
let mut cursor = 0;
while cursor < bytes.len() {
while cursor < bytes.len() && bytes[cursor] == 0 {
cursor += 1;
}
if cursor == bytes.len() {
break;
}
let start = cursor;
while cursor < bytes.len() && bytes[cursor] != 0 {
cursor += 1;
}
dst.seek(SeekFrom::Start(base_offset + start as u64))?;
dst.write_all(&bytes[start..cursor])?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Seek, SeekFrom, Write};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
fn make_sparse(path: &Path, len: u64, data_offsets: &[u64]) -> io::Result<()> {
let mut f = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
f.set_len(len)?;
for &off in data_offsets {
let buf = vec![0xAB_u8; 64 * 1024];
f.seek(SeekFrom::Start(off))?;
f.write_all(&buf)?;
}
f.sync_all()?;
Ok(())
}
#[test]
fn round_trip_small() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src.bin");
let dst = dir.path().join("dst.bin");
std::fs::write(&src, b"hello world").unwrap();
let n = fast_copy(&src, &dst).unwrap();
assert_eq!(n, 11);
assert_eq!(std::fs::read(&dst).unwrap(), b"hello world");
}
#[test]
fn sparse_copy_preserves_holes_and_data() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src.bin");
let dst = dir.path().join("dst.bin");
let len: u64 = 16 * 1024 * 1024;
let offsets = [0u64, 4 * 1024 * 1024, 8 * 1024 * 1024, 12 * 1024 * 1024];
make_sparse(&src, len, &offsets).unwrap();
let n = sparse_copy(&src, &dst).unwrap();
assert_eq!(n, len);
let dst_meta = std::fs::metadata(&dst).unwrap();
assert_eq!(dst_meta.len(), len);
let mut buf = [0u8; 64 * 1024];
let mut dst_file = File::open(&dst).unwrap();
for &off in &offsets {
dst_file.seek(SeekFrom::Start(off)).unwrap();
dst_file.read_exact(&mut buf).unwrap();
assert!(buf.iter().all(|&b| b == 0xAB));
}
#[cfg(unix)]
{
let src_bytes_on_disk = std::fs::metadata(&src).unwrap().blocks() * 512;
let dst_bytes_on_disk = dst_meta.blocks() * 512;
if src_bytes_on_disk < len / 2 {
assert!(
dst_bytes_on_disk < len / 2,
"source is sparse ({src_bytes_on_disk} bytes on disk) but destination densified to {dst_bytes_on_disk} bytes for an apparent size of {len}",
);
assert!(
dst_bytes_on_disk <= src_bytes_on_disk * 4 + 1024 * 1024,
"destination allocated significantly more than source: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
);
} else {
eprintln!(
"filesystem did not sparsify the source (src_bytes_on_disk={src_bytes_on_disk}, apparent={len}); sparseness preservation not exercised in this run",
);
assert!(
dst_bytes_on_disk <= src_bytes_on_disk + 1024 * 1024,
"destination grew beyond source footprint: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
);
}
}
}
#[test]
fn fast_copy_matches_source_size() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src.bin");
let dst = dir.path().join("dst.bin");
let len: u64 = 4 * 1024 * 1024;
make_sparse(&src, len, &[0, 2 * 1024 * 1024]).unwrap();
let n = fast_copy(&src, &dst).unwrap();
assert_eq!(n, len);
assert_eq!(std::fs::metadata(&dst).unwrap().len(), len);
}
#[test]
fn missing_source_errors() {
let dir = tempfile::tempdir().unwrap();
let err = fast_copy(&dir.path().join("nope.bin"), &dir.path().join("dst.bin")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
}