From 904094781e84a7c60902ad085baeb1eaf722162f Mon Sep 17 00:00:00 2001 From: Paul Richards Date: Wed, 13 May 2026 22:24:46 +0100 Subject: [PATCH] feat: Add `fs::ioctl_ficlonerange` for FICLONERANGE Alongside `fs::ioctl_ficlone` for FICLONE, add support for the FICLONERANGE operation. --- src/backend/linux_raw/c.rs | 2 +- src/fs/ioctl.rs | 67 ++++++++++++++++++++++++++++++++++++++ tests/fs/ioctl.rs | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/backend/linux_raw/c.rs b/src/backend/linux_raw/c.rs index 762cdd479..a70ab2250 100644 --- a/src/backend/linux_raw/c.rs +++ b/src/backend/linux_raw/c.rs @@ -63,7 +63,7 @@ pub(crate) use linux_raw_sys::general::{ XATTR_REPLACE, }; -pub(crate) use linux_raw_sys::ioctl::{BLKPBSZGET, BLKSSZGET, FICLONE}; +pub(crate) use linux_raw_sys::ioctl::{BLKPBSZGET, BLKSSZGET, FICLONE, FICLONERANGE}; #[cfg(target_pointer_width = "32")] pub(crate) use linux_raw_sys::ioctl::{FS_IOC32_GETFLAGS, FS_IOC32_SETFLAGS}; #[cfg(target_pointer_width = "64")] diff --git a/src/fs/ioctl.rs b/src/fs/ioctl.rs index 16e0dda12..187a6f2dd 100644 --- a/src/fs/ioctl.rs +++ b/src/fs/ioctl.rs @@ -2,6 +2,9 @@ #![allow(unsafe_code)] +#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))] +use std::{marker::PhantomData, os::fd::OwnedFd}; + #[cfg(linux_kernel)] use { crate::backend::c, @@ -57,6 +60,38 @@ pub fn ioctl_ficlone(fd: Fd, src_fd: SrcFd) -> io::Result unsafe { ioctl::ioctl(fd, Ficlone(src_fd.as_fd())) } } +/// `ioctl(fd, FICLONERANGE, ...)`—share some the data of one file with another file. +/// +/// This ioctl is not available on SPARC platforms. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html +#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))] +#[inline] +#[doc(alias = "FICLONERANGE")] +pub fn ioctl_ficlonerange( + fd: Fd, + src_fd: SrcFd, + src_offset: u64, + src_length: u64, + dest_offset: u64, +) -> io::Result<()> { + unsafe { + ioctl::ioctl( + fd, + Ficlonerange { + src_fd: i64::from(src_fd.as_fd().as_raw_fd()), + src_offset, + src_length, + dest_offset, + _phantom: PhantomData, + }, + ) + } +} + /// `ioctl(fd, EXT4_IOC_RESIZE_FS, blocks)`—Resize ext4 filesystem on fd. #[cfg(linux_raw_dep)] #[inline] @@ -94,6 +129,38 @@ unsafe impl ioctl::Ioctl for Ficlone<'_> { } } +#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))] +#[repr(C)] +struct Ficlonerange<'a> { + src_fd: i64, + src_offset: u64, + src_length: u64, + dest_offset: u64, + _phantom: PhantomData<&'a OwnedFd>, +} + +#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))] +unsafe impl ioctl::Ioctl for Ficlonerange<'_> { + type Output = (); + + const IS_MUTATING: bool = false; + + fn opcode(&self) -> ioctl::Opcode { + c::FICLONERANGE as ioctl::Opcode + } + + fn as_ptr(&mut self) -> *mut c::c_void { + std::ptr::from_mut(self) as *mut c::c_void + } + + unsafe fn output_from_ptr( + _: ioctl::IoctlOutput, + _: *mut c::c_void, + ) -> io::Result { + Ok(()) + } +} + #[cfg(linux_raw_dep)] bitflags! { /// `FS_*` constants for use with [`ioctl_getflags`]. diff --git a/tests/fs/ioctl.rs b/tests/fs/ioctl.rs index d84d8eb0f..a0396782e 100644 --- a/tests/fs/ioctl.rs +++ b/tests/fs/ioctl.rs @@ -23,3 +23,60 @@ fn test_ioctl_ficlone() { Err(err) => panic!("{:?}", err), } } + +#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))] +#[test] +fn test_ioctl_ficlonerange() { + use rustix::io; + + let src = std::fs::File::open("Cargo.toml").unwrap(); + let dest = tempfile::tempfile().unwrap(); + + // Often the temporary directory is on a different filesystem (like tmpfs), + // which means the test to clone some data doesn't do anything interesting, + // singe the ioctl simply returns failure. + // Uncomment the line below to use a file in the same directory as the source, + // which guarantees they are on the same filesystem. + // let dest = std::fs::File::options() + // .create(true) + // .truncate(true) + // .read(true) + // .write(true) + // .open("test_ficlonerange").unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let dir = std::fs::File::open(dir.path()).unwrap(); + + // `src` isn't opened for writing, so passing it as the output fails. + assert_eq!( + rustix::fs::ioctl_ficlonerange(&src, &src, 0, 4096, 0), + Err(io::Errno::BADF) + ); + + // `FICLONERANGE` operates on regular files, not directories. + assert_eq!( + rustix::fs::ioctl_ficlonerange(&dir, &dir, 0, 4096, 0), + Err(io::Errno::ISDIR) + ); + + // Now try something that might succeed, though be prepared for filesystems + // that don't support this. + // Copy 4096 bytes from offset 4096 in src to offset 8192 in dest. + match rustix::fs::ioctl_ficlonerange(&dest, &src, 4096, 4096, 8192) { + Ok(()) => { + use std::os::unix::fs::FileExt; + + let mut expected_buf = vec![0u8; 4096]; + let mut actual_buf = vec![0u8; 4096]; + src.read_exact_at(expected_buf.as_mut_slice(), 4096) + .unwrap(); + dest.read_exact_at(actual_buf.as_mut_slice(), 8192).unwrap(); + + assert_eq!(expected_buf, actual_buf); + } + + Err(io::Errno::OPNOTSUPP) => (), + Err(e) if e == io::Errno::from_raw_os_error(0x12) => (), + Err(err) => panic!("{:?}", err), + } +}