Skip to content

Commit 9040947

Browse files
committed
feat: Add fs::ioctl_ficlonerange for FICLONERANGE
Alongside `fs::ioctl_ficlone` for FICLONE, add support for the FICLONERANGE operation.
1 parent 9f588bc commit 9040947

3 files changed

Lines changed: 125 additions & 1 deletion

File tree

src/backend/linux_raw/c.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub(crate) use linux_raw_sys::general::{
6363
XATTR_REPLACE,
6464
};
6565

66-
pub(crate) use linux_raw_sys::ioctl::{BLKPBSZGET, BLKSSZGET, FICLONE};
66+
pub(crate) use linux_raw_sys::ioctl::{BLKPBSZGET, BLKSSZGET, FICLONE, FICLONERANGE};
6767
#[cfg(target_pointer_width = "32")]
6868
pub(crate) use linux_raw_sys::ioctl::{FS_IOC32_GETFLAGS, FS_IOC32_SETFLAGS};
6969
#[cfg(target_pointer_width = "64")]

src/fs/ioctl.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
33
#![allow(unsafe_code)]
44

5+
#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))]
6+
use std::{marker::PhantomData, os::fd::OwnedFd};
7+
58
#[cfg(linux_kernel)]
69
use {
710
crate::backend::c,
@@ -57,6 +60,38 @@ pub fn ioctl_ficlone<Fd: AsFd, SrcFd: AsFd>(fd: Fd, src_fd: SrcFd) -> io::Result
5760
unsafe { ioctl::ioctl(fd, Ficlone(src_fd.as_fd())) }
5861
}
5962

63+
/// `ioctl(fd, FICLONERANGE, ...)`—share some the data of one file with another file.
64+
///
65+
/// This ioctl is not available on SPARC platforms.
66+
///
67+
/// # References
68+
/// - [Linux]
69+
///
70+
/// [Linux]: https://man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html
71+
#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))]
72+
#[inline]
73+
#[doc(alias = "FICLONERANGE")]
74+
pub fn ioctl_ficlonerange<Fd: AsFd, SrcFd: AsFd>(
75+
fd: Fd,
76+
src_fd: SrcFd,
77+
src_offset: u64,
78+
src_length: u64,
79+
dest_offset: u64,
80+
) -> io::Result<()> {
81+
unsafe {
82+
ioctl::ioctl(
83+
fd,
84+
Ficlonerange {
85+
src_fd: i64::from(src_fd.as_fd().as_raw_fd()),
86+
src_offset,
87+
src_length,
88+
dest_offset,
89+
_phantom: PhantomData,
90+
},
91+
)
92+
}
93+
}
94+
6095
/// `ioctl(fd, EXT4_IOC_RESIZE_FS, blocks)`—Resize ext4 filesystem on fd.
6196
#[cfg(linux_raw_dep)]
6297
#[inline]
@@ -94,6 +129,38 @@ unsafe impl ioctl::Ioctl for Ficlone<'_> {
94129
}
95130
}
96131

132+
#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))]
133+
#[repr(C)]
134+
struct Ficlonerange<'a> {
135+
src_fd: i64,
136+
src_offset: u64,
137+
src_length: u64,
138+
dest_offset: u64,
139+
_phantom: PhantomData<&'a OwnedFd>,
140+
}
141+
142+
#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))]
143+
unsafe impl ioctl::Ioctl for Ficlonerange<'_> {
144+
type Output = ();
145+
146+
const IS_MUTATING: bool = false;
147+
148+
fn opcode(&self) -> ioctl::Opcode {
149+
c::FICLONERANGE as ioctl::Opcode
150+
}
151+
152+
fn as_ptr(&mut self) -> *mut c::c_void {
153+
std::ptr::from_mut(self) as *mut c::c_void
154+
}
155+
156+
unsafe fn output_from_ptr(
157+
_: ioctl::IoctlOutput,
158+
_: *mut c::c_void,
159+
) -> io::Result<Self::Output> {
160+
Ok(())
161+
}
162+
}
163+
97164
#[cfg(linux_raw_dep)]
98165
bitflags! {
99166
/// `FS_*` constants for use with [`ioctl_getflags`].

tests/fs/ioctl.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,60 @@ fn test_ioctl_ficlone() {
2323
Err(err) => panic!("{:?}", err),
2424
}
2525
}
26+
27+
#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))]
28+
#[test]
29+
fn test_ioctl_ficlonerange() {
30+
use rustix::io;
31+
32+
let src = std::fs::File::open("Cargo.toml").unwrap();
33+
let dest = tempfile::tempfile().unwrap();
34+
35+
// Often the temporary directory is on a different filesystem (like tmpfs),
36+
// which means the test to clone some data doesn't do anything interesting,
37+
// singe the ioctl simply returns failure.
38+
// Uncomment the line below to use a file in the same directory as the source,
39+
// which guarantees they are on the same filesystem.
40+
// let dest = std::fs::File::options()
41+
// .create(true)
42+
// .truncate(true)
43+
// .read(true)
44+
// .write(true)
45+
// .open("test_ficlonerange").unwrap();
46+
47+
let dir = tempfile::tempdir().unwrap();
48+
let dir = std::fs::File::open(dir.path()).unwrap();
49+
50+
// `src` isn't opened for writing, so passing it as the output fails.
51+
assert_eq!(
52+
rustix::fs::ioctl_ficlonerange(&src, &src, 0, 4096, 0),
53+
Err(io::Errno::BADF)
54+
);
55+
56+
// `FICLONERANGE` operates on regular files, not directories.
57+
assert_eq!(
58+
rustix::fs::ioctl_ficlonerange(&dir, &dir, 0, 4096, 0),
59+
Err(io::Errno::ISDIR)
60+
);
61+
62+
// Now try something that might succeed, though be prepared for filesystems
63+
// that don't support this.
64+
// Copy 4096 bytes from offset 4096 in src to offset 8192 in dest.
65+
match rustix::fs::ioctl_ficlonerange(&dest, &src, 4096, 4096, 8192) {
66+
Ok(()) => {
67+
use std::os::unix::fs::FileExt;
68+
69+
let mut expected_buf = vec![0u8; 4096];
70+
let mut actual_buf = vec![0u8; 4096];
71+
src.read_exact_at(expected_buf.as_mut_slice(), 4096)
72+
.unwrap();
73+
dest.read_exact_at(actual_buf.as_mut_slice(), 8192).unwrap();
74+
75+
assert_eq!(expected_buf, actual_buf);
76+
}
77+
78+
Err(io::Errno::OPNOTSUPP) => (),
79+
Err(e) if e == io::Errno::from_raw_os_error(0x12) => (),
80+
Err(err) => panic!("{:?}", err),
81+
}
82+
}

0 commit comments

Comments
 (0)