use libc::{SEEK_DATA, SEEK_HOLE};
use std::fs::{File, OpenOptions};
use std::io::Read;
use std::os::unix::fs::FileExt;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::{FileTypeExt, OpenOptionsExt};
use std::os::unix::io::AsRawFd;
use std::path::Path;
use uucore::buf_copy;
use quick_error::ResultExt;
use uucore::mode::get_umask;
use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode};
macro_rules! FICLONE {
() => {
0x40049409
};
}
#[derive(Clone, Copy)]
enum CloneFallback {
Error,
FSCopy,
SparseCopy,
SparseCopyWithoutHole,
}
#[derive(Clone, Copy)]
enum CopyMethod {
SparseCopy,
FSCopy,
Default,
SparseCopyWithoutHole,
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn clone<P>(source: P, dest: P, fallback: CloneFallback) -> std::io::Result<()>
where
P: AsRef<Path>,
{
let src_file = File::open(&source)?;
let dst_file = File::create(&dest)?;
let src_fd = src_file.as_raw_fd();
let dst_fd = dst_file.as_raw_fd();
let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) };
if result == 0 {
return Ok(());
}
match fallback {
CloneFallback::Error => Err(std::io::Error::last_os_error()),
CloneFallback::FSCopy => std::fs::copy(source, dest).map(|_| ()),
CloneFallback::SparseCopy => sparse_copy(source, dest),
CloneFallback::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest),
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn check_for_data(source: &Path) -> Result<(bool, u64, u64), std::io::Error> {
let mut src_file = File::open(source)?;
let metadata = src_file.metadata()?;
let size = metadata.size();
let blocks = metadata.blocks();
if size == 0 {
let mut buf: Vec<u8> = vec![0; metadata.blksize() as usize]; let _ = src_file.read(&mut buf)?;
return Ok((buf.iter().any(|&x| x != 0x0), size, 0));
}
let src_fd = src_file.as_raw_fd();
let result = unsafe { libc::lseek(src_fd, 0, SEEK_DATA) };
match result {
-1 => Ok((false, size, blocks)), _ if result >= 0 => Ok((true, size, blocks)), _ => Err(std::io::Error::last_os_error()),
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn check_sparse_detection(source: &Path) -> Result<bool, std::io::Error> {
let src_file = File::open(source)?;
let metadata = src_file.metadata()?;
let size = metadata.size();
let blocks = metadata.blocks();
if blocks < size / 512 {
return Ok(true);
}
Ok(false)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn sparse_copy_without_hole<P>(source: P, dest: P) -> std::io::Result<()>
where
P: AsRef<Path>,
{
let src_file = File::open(source)?;
let dst_file = File::create(dest)?;
let dst_fd = dst_file.as_raw_fd();
let size = src_file.metadata()?.size();
if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
return Err(std::io::Error::last_os_error());
}
let src_fd = src_file.as_raw_fd();
let mut current_offset: isize = 0;
let step = std::cmp::min(size, 16 * 1024 * 1024) as usize;
let mut buf: Vec<u8> = vec![0x0; step];
loop {
let result = unsafe { libc::lseek(src_fd, current_offset.try_into().unwrap(), SEEK_DATA) }
.try_into()
.unwrap();
current_offset = result;
let hole: isize =
unsafe { libc::lseek(src_fd, current_offset.try_into().unwrap(), SEEK_HOLE) }
.try_into()
.unwrap();
if result == -1 || hole == -1 {
break;
}
if result <= -2 || hole <= -2 {
return Err(std::io::Error::last_os_error());
}
let len: isize = hole - current_offset;
for i in (0..len).step_by(step) {
let read_len = std::cmp::min((len - i) as usize, step);
let buf = &mut buf[..read_len];
src_file.read_exact_at(buf, (current_offset + i) as u64)?;
dst_file.write_all_at(buf, (current_offset + i) as u64)?;
}
current_offset = hole;
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn sparse_copy<P>(source: P, dest: P) -> std::io::Result<()>
where
P: AsRef<Path>,
{
let mut src_file = File::open(source)?;
let dst_file = File::create(dest)?;
let dst_fd = dst_file.as_raw_fd();
let size: usize = src_file.metadata()?.size().try_into().unwrap();
if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
return Err(std::io::Error::last_os_error());
}
let blksize = dst_file.metadata()?.blksize();
let mut buf: Vec<u8> = vec![0; blksize.try_into().unwrap()];
let mut current_offset: usize = 0;
while current_offset < size {
let this_read = src_file.read(&mut buf)?;
let buf = &buf[..this_read];
if buf.iter().any(|&x| x != 0) {
dst_file.write_all_at(buf, current_offset.try_into().unwrap())?;
}
current_offset += this_read;
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn check_dest_is_fifo(dest: &Path) -> bool {
let file_type = std::fs::metadata(dest);
match file_type {
Ok(f) => f.file_type().is_fifo(),
_ => false,
}
}
fn copy_stream<P>(source: P, dest: P, is_fifo: bool) -> std::io::Result<u64>
where
P: AsRef<Path>,
{
let mut src_file = File::open(&source)?;
let mode = 0o622 & !get_umask();
let mut dst_file = OpenOptions::new()
.create(true)
.write(true)
.mode(mode)
.open(&dest)?;
let num_bytes_copied = buf_copy::copy_stream(&mut src_file, &mut dst_file)
.map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?;
if is_fifo {
dst_file.set_permissions(src_file.metadata()?.permissions())?;
}
Ok(num_bytes_copied)
}
pub(crate) fn copy_on_write(
source: &Path,
dest: &Path,
reflink_mode: ReflinkMode,
sparse_mode: SparseMode,
context: &str,
source_is_fifo: bool,
source_is_stream: bool,
) -> CopyResult<CopyDebug> {
let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown,
reflink: OffloadReflinkDebug::Unsupported,
sparse_detection: SparseDebug::No,
};
let result = match (reflink_mode, sparse_mode) {
(ReflinkMode::Never, SparseMode::Always) => {
copy_debug.sparse_detection = SparseDebug::Zeros;
copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
} else {
let mut copy_method = CopyMethod::Default;
let result = handle_reflink_never_sparse_always(source, dest);
if let Ok((debug, method)) = result {
copy_debug = debug;
copy_method = method;
}
match copy_method {
CopyMethod::FSCopy => std::fs::copy(source, dest).map(|_| ()),
_ => sparse_copy(source, dest),
}
}
}
(ReflinkMode::Never, SparseMode::Never) => {
copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
} else {
let result = handle_reflink_never_sparse_never(source);
if let Ok(debug) = result {
copy_debug = debug;
}
std::fs::copy(source, dest).map(|_| ())
}
}
(ReflinkMode::Never, SparseMode::Auto) => {
copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
} else {
let mut copy_method = CopyMethod::Default;
let result = handle_reflink_never_sparse_auto(source, dest);
if let Ok((debug, method)) = result {
copy_debug = debug;
copy_method = method;
}
match copy_method {
CopyMethod::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest),
_ => std::fs::copy(source, dest).map(|_| ()),
}
}
}
(ReflinkMode::Auto, SparseMode::Always) => {
copy_debug.sparse_detection = SparseDebug::Zeros; if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
} else {
let mut copy_method = CopyMethod::Default;
let result = handle_reflink_auto_sparse_always(source, dest);
if let Ok((debug, method)) = result {
copy_debug = debug;
copy_method = method;
}
match copy_method {
CopyMethod::FSCopy => clone(source, dest, CloneFallback::FSCopy),
_ => clone(source, dest, CloneFallback::SparseCopy),
}
}
}
(ReflinkMode::Auto, SparseMode::Never) => {
copy_debug.reflink = OffloadReflinkDebug::No;
if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Avoided;
copy_stream(source, dest, source_is_fifo).map(|_| ())
} else {
let result = handle_reflink_auto_sparse_never(source);
if let Ok(debug) = result {
copy_debug = debug;
}
clone(source, dest, CloneFallback::FSCopy)
}
}
(ReflinkMode::Auto, SparseMode::Auto) => {
if source_is_stream {
copy_debug.offload = OffloadReflinkDebug::Unsupported;
copy_stream(source, dest, source_is_fifo).map(|_| ())
} else {
let mut copy_method = CopyMethod::Default;
let result = handle_reflink_auto_sparse_auto(source, dest);
if let Ok((debug, method)) = result {
copy_debug = debug;
copy_method = method;
}
match copy_method {
CopyMethod::SparseCopyWithoutHole => {
clone(source, dest, CloneFallback::SparseCopyWithoutHole)
}
_ => clone(source, dest, CloneFallback::FSCopy),
}
}
}
(ReflinkMode::Always, SparseMode::Auto) => {
copy_debug.sparse_detection = SparseDebug::No;
copy_debug.reflink = OffloadReflinkDebug::Yes;
clone(source, dest, CloneFallback::Error)
}
(ReflinkMode::Always, _) => {
return Err("`--reflink=always` can be used only with --sparse=auto".into())
}
};
result.context(context)?;
Ok(copy_debug)
}
fn handle_reflink_auto_sparse_always(
source: &Path,
dest: &Path,
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown,
reflink: OffloadReflinkDebug::Unsupported,
sparse_detection: SparseDebug::Zeros,
};
let mut copy_method = CopyMethod::Default;
let (data_flag, size, blocks) = check_for_data(source)?;
let sparse_flag = check_sparse_detection(source)?;
if data_flag || size < 512 {
copy_debug.offload = OffloadReflinkDebug::Avoided;
}
match (sparse_flag, data_flag, blocks) {
(true, true, 0) => {
copy_method = CopyMethod::FSCopy;
copy_debug.sparse_detection = SparseDebug::SeekHoleZeros;
}
(false, true, 0) => copy_method = CopyMethod::FSCopy,
(true, false, 0) => copy_debug.sparse_detection = SparseDebug::SeekHole,
(true, true, _) => copy_debug.sparse_detection = SparseDebug::SeekHoleZeros,
(true, false, _) => copy_debug.sparse_detection = SparseDebug::SeekHole,
(_, _, _) => (),
}
if check_dest_is_fifo(dest) {
copy_method = CopyMethod::FSCopy;
}
Ok((copy_debug, copy_method))
}
fn handle_reflink_never_sparse_never(source: &Path) -> Result<CopyDebug, std::io::Error> {
let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown,
reflink: OffloadReflinkDebug::No,
sparse_detection: SparseDebug::No,
};
let (data_flag, size, _blocks) = check_for_data(source)?;
let sparse_flag = check_sparse_detection(source)?;
if sparse_flag {
copy_debug.sparse_detection = SparseDebug::SeekHole;
}
if data_flag || size < 512 {
copy_debug.offload = OffloadReflinkDebug::Avoided;
}
Ok(copy_debug)
}
fn handle_reflink_auto_sparse_never(source: &Path) -> Result<CopyDebug, std::io::Error> {
let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown,
reflink: OffloadReflinkDebug::No,
sparse_detection: SparseDebug::No,
};
let (data_flag, size, _blocks) = check_for_data(source)?;
let sparse_flag = check_sparse_detection(source)?;
if sparse_flag {
copy_debug.sparse_detection = SparseDebug::SeekHole;
}
if data_flag || size < 512 {
copy_debug.offload = OffloadReflinkDebug::Avoided;
}
Ok(copy_debug)
}
fn handle_reflink_auto_sparse_auto(
source: &Path,
dest: &Path,
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown,
reflink: OffloadReflinkDebug::Unsupported,
sparse_detection: SparseDebug::No,
};
let mut copy_method = CopyMethod::Default;
let (data_flag, size, blocks) = check_for_data(source)?;
let sparse_flag = check_sparse_detection(source)?;
if (data_flag && size != 0) || (size > 0 && size < 512) {
copy_debug.offload = OffloadReflinkDebug::Yes;
}
if data_flag && size == 0 {
copy_debug.offload = OffloadReflinkDebug::Unsupported;
}
if sparse_flag {
if blocks == 0 && data_flag {
copy_debug.offload = OffloadReflinkDebug::Unsupported;
copy_method = CopyMethod::FSCopy; } else {
copy_method = CopyMethod::SparseCopyWithoutHole;
} copy_debug.sparse_detection = SparseDebug::SeekHole;
}
if check_dest_is_fifo(dest) {
copy_method = CopyMethod::FSCopy;
}
Ok((copy_debug, copy_method))
}
fn handle_reflink_never_sparse_auto(
source: &Path,
dest: &Path,
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown,
reflink: OffloadReflinkDebug::No,
sparse_detection: SparseDebug::No,
};
let (data_flag, size, blocks) = check_for_data(source)?;
let sparse_flag = check_sparse_detection(source)?;
let mut copy_method = CopyMethod::Default;
if data_flag || size < 512 {
copy_debug.offload = OffloadReflinkDebug::Avoided;
}
if sparse_flag {
if blocks == 0 && data_flag {
copy_method = CopyMethod::FSCopy; } else {
copy_method = CopyMethod::SparseCopyWithoutHole; }
copy_debug.sparse_detection = SparseDebug::SeekHole;
}
if check_dest_is_fifo(dest) {
copy_method = CopyMethod::FSCopy;
}
Ok((copy_debug, copy_method))
}
fn handle_reflink_never_sparse_always(
source: &Path,
dest: &Path,
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
let mut copy_debug = CopyDebug {
offload: OffloadReflinkDebug::Unknown,
reflink: OffloadReflinkDebug::No,
sparse_detection: SparseDebug::Zeros,
};
let mut copy_method = CopyMethod::SparseCopy;
let (data_flag, size, blocks) = check_for_data(source)?;
let sparse_flag = check_sparse_detection(source)?;
if data_flag || size < 512 {
copy_debug.offload = OffloadReflinkDebug::Avoided;
}
match (sparse_flag, data_flag, blocks) {
(true, true, 0) => {
copy_method = CopyMethod::FSCopy;
copy_debug.sparse_detection = SparseDebug::SeekHoleZeros;
}
(false, true, 0) => copy_method = CopyMethod::FSCopy, (true, false, 0) => copy_debug.sparse_detection = SparseDebug::SeekHole, (true, true, _) => copy_debug.sparse_detection = SparseDebug::SeekHoleZeros, (true, false, _) => {
copy_debug.offload = OffloadReflinkDebug::Unknown;
copy_debug.sparse_detection = SparseDebug::SeekHole;
}
(_, _, _) => (),
}
if check_dest_is_fifo(dest) {
copy_method = CopyMethod::FSCopy;
}
Ok((copy_debug, copy_method))
}