use std::fs::File;
use std::io;
#[cfg(target_os = "linux")]
fn permits_fallback(error: rustix::io::Errno) -> bool {
use rustix::io::Errno;
matches!(
error,
Errno::NOSYS | Errno::XDEV | Errno::OPNOTSUPP | Errno::INVAL | Errno::PERM
)
}
#[cfg(target_os = "linux")]
pub(super) fn copy_range(
source: &File,
destination: &File,
offset: u64,
length: usize,
) -> io::Result<Option<usize>> {
let mut input = offset;
let mut output = offset;
match rustix::fs::copy_file_range(
source,
Some(&mut input),
destination,
Some(&mut output),
length,
) {
Ok(0) if input == offset && output == offset => Ok(None),
Ok(copied) => {
let end = offset.checked_add(copied as u64).ok_or_else(|| {
io::Error::other("publication range offset overflow; evidence retained")
})?;
if copied > length || input != end || output != end {
return Err(io::Error::other(
"publication range offsets disagree; evidence retained",
));
}
Ok(Some(copied))
}
Err(error) if permits_fallback(error) && input == offset && output == offset => Ok(None),
Err(error) => Err(error.into()),
}
}
#[cfg(not(target_os = "linux"))]
pub(super) fn copy_range(
_source: &File,
_destination: &File,
_offset: u64,
_length: usize,
) -> io::Result<Option<usize>> {
Ok(None)
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
use std::io::{Seek, SeekFrom};
use std::os::unix::fs::{FileExt, MetadataExt};
#[test]
fn only_unsupported_range_operations_allow_fallback() {
use rustix::io::Errno;
for error in [
Errno::NOSYS,
Errno::XDEV,
Errno::OPNOTSUPP,
Errno::INVAL,
Errno::PERM,
] {
assert!(permits_fallback(error));
}
for error in [Errno::NOSPC, Errno::IO, Errno::ACCESS, Errno::INTR] {
assert!(!permits_fallback(error));
}
}
#[test]
fn native_range_copy_preserves_offsets_or_reports_unsupported_without_writing() {
let root = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.unwrap();
let bytes = vec![42; 131_077];
let path = root.path().join("source");
std::fs::write(&path, &bytes).unwrap();
let mut source = File::open(&path).unwrap();
let destination_path = root.path().join("destination");
let mut destination = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&destination_path)
.unwrap();
destination.write_all_at(b"retained-prefix", 0).unwrap();
let inode = destination.metadata().unwrap().ino();
let prefix = b"retained-prefix".len() as u64;
source.seek(SeekFrom::Start(3)).unwrap();
destination.seek(SeekFrom::Start(5)).unwrap();
let mut offset = prefix;
while offset < bytes.len() as u64 {
let length = (bytes.len() as u64 - offset).min(65_536) as usize;
match copy_range(&source, &destination, offset, length).unwrap() {
Some(copied) => {
assert!(copied > 0 && copied <= length);
offset += copied as u64;
}
None => {
assert_eq!(destination.metadata().unwrap().len(), offset);
break;
}
}
}
assert_eq!(source.stream_position().unwrap(), 3);
assert_eq!(destination.stream_position().unwrap(), 5);
assert_eq!(std::fs::metadata(&destination_path).unwrap().ino(), inode);
let content = std::fs::read(&destination_path).unwrap();
assert_eq!(&content[..prefix as usize], b"retained-prefix");
assert_eq!(
&content[prefix as usize..],
&bytes[prefix as usize..offset as usize]
);
assert_eq!(std::fs::read(&path).unwrap(), bytes);
}
}