Skip to content

Commit 10e24e5

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 10e24e5

3 files changed

Lines changed: 127 additions & 2 deletions

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: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use {
1212
use bitflags::bitflags;
1313

1414
#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))]
15-
use crate::fd::{AsRawFd as _, BorrowedFd};
15+
use {
16+
crate::fd::{AsRawFd as _, BorrowedFd, OwnedFd},
17+
core::marker::PhantomData,
18+
};
1619

1720
/// `ioctl(fd, BLKSSZGET)`—Returns the logical block size of a block device.
1821
///
@@ -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,39 @@ 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+
// Since `src_fd` must be 64-bit, we cannot use `BorrowedFd` like we do in `Ficlone`.
136+
src_fd: i64,
137+
src_offset: u64,
138+
src_length: u64,
139+
dest_offset: u64,
140+
_phantom: PhantomData<&'a OwnedFd>,
141+
}
142+
143+
#[cfg(all(linux_kernel, not(any(target_arch = "sparc", target_arch = "sparc64"))))]
144+
unsafe impl ioctl::Ioctl for Ficlonerange<'_> {
145+
type Output = ();
146+
147+
const IS_MUTATING: bool = false;
148+
149+
fn opcode(&self) -> ioctl::Opcode {
150+
c::FICLONERANGE as ioctl::Opcode
151+
}
152+
153+
fn as_ptr(&mut self) -> *mut c::c_void {
154+
self as *mut Self as *mut c::c_void
155+
}
156+
157+
unsafe fn output_from_ptr(
158+
_: ioctl::IoctlOutput,
159+
_: *mut c::c_void,
160+
) -> io::Result<Self::Output> {
161+
Ok(())
162+
}
163+
}
164+
97165
#[cfg(linux_raw_dep)]
98166
bitflags! {
99167
/// `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)