Skip to content

Commit bd40e9e

Browse files
committed
fast_io.rs: replace the unsafe libc::copy_file_range call by rustix
Replace the unsafe `libc::copy_file_range` call with the safe `rustix::fs::copy_file_range`. Closes: #442
1 parent b90c599 commit bd40e9e

3 files changed

Lines changed: 148 additions & 44 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ tempfile = "3.10.1"
5353
terminal_size = "0.4.2"
5454
textwrap = { version = "0.16.1", features = ["terminal_size"] }
5555
uucore = { version = "0.9.0", features = ["libc"] }
56+
rustix = "1.1.4"
5657
xattr = "1.3.1"
5758

5859

@@ -71,6 +72,7 @@ tempfile = { workspace = true }
7172
terminal_size = { workspace = true }
7273
textwrap = { workspace = true }
7374
uucore = { workspace = true }
75+
rustix = { workspace = true }
7476

7577
[dev-dependencies]
7678
assert_fs = { workspace = true }

src/sed/fast_io.rs

Lines changed: 145 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ use std::marker::PhantomData;
2929
#[cfg(unix)]
3030
use std::os::fd::RawFd;
3131

32+
#[cfg(unix)]
33+
use rustix::fd::BorrowedFd;
34+
#[cfg(target_os = "linux")]
35+
use rustix::fs::copy_file_range as rustix_copy_file_range;
36+
3237
#[cfg(unix)]
3338
use std::os::unix::io::AsRawFd;
3439

@@ -392,6 +397,19 @@ impl FastCopy {
392397
block_size: st.st_blksize as usize,
393398
}
394399
}
400+
401+
/// Return a borrowed file descriptor.
402+
///
403+
/// # Safety invariant
404+
///
405+
/// `FastCopy` only stores file descriptors obtained from open
406+
/// [`File`](std::fs::File) objects. Those `File`s are kept alive
407+
/// by the owning [`MmapLineCursor`] / output-buffer, guaranteeing
408+
/// the fd remains valid for the duration of any borrow.
409+
pub fn as_fd(&self) -> BorrowedFd<'_> {
410+
// SAFETY: self.fd is a valid file descriptor owned by a live File.
411+
unsafe { BorrowedFd::borrow_raw(self.fd) }
412+
}
395413
}
396414

397415
#[cfg(test)]
@@ -774,10 +792,10 @@ impl OutputBuffer {
774792
if chunk.in_fast_copy.is_regular && self.fast_copy.is_regular {
775793
portable_copy_file_range(
776794
chunk.out_ptr,
777-
chunk.in_fast_copy.fd,
795+
chunk.in_fast_copy.as_fd(),
778796
// Input file offset
779797
unsafe { chunk.out_ptr.offset_from(chunk.base_ptr) } as libc::off_t,
780-
self.fast_copy.fd,
798+
self.fast_copy.as_fd(),
781799
chunk.len,
782800
// Alignment block size: the largest of the two
783801
chunk.in_fast_copy.block_size.max(self.fast_copy.block_size),
@@ -892,9 +910,9 @@ fn reliable_write(fd: i32, ptr: *const u8, len: usize) -> std::io::Result<usize>
892910
#[allow(unused_variables)]
893911
fn portable_copy_file_range(
894912
in_ptr: *const u8,
895-
in_fd: i32,
913+
in_fd: BorrowedFd<'_>,
896914
in_off: libc::off_t,
897-
out_fd: i32,
915+
out_fd: BorrowedFd<'_>,
898916
len: usize,
899917
block_size: usize,
900918
cover: WriteRange,
@@ -909,7 +927,7 @@ fn portable_copy_file_range(
909927
}
910928
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
911929
{
912-
reliable_write(out_fd, in_ptr, len)
930+
reliable_write(out_fd.as_raw_fd(), in_ptr, len)
913931
}
914932
}
915933

@@ -920,37 +938,34 @@ fn portable_copy_file_range(
920938
#[cfg(all(target_os = "linux", target_env = "gnu"))]
921939
fn reliable_copy_file_range(
922940
in_ptr: *const u8,
923-
in_fd: i32,
941+
in_fd: BorrowedFd<'_>,
924942
mut in_off: libc::off_t,
925-
out_fd: i32,
943+
out_fd: BorrowedFd<'_>,
926944
len: usize,
927945
) -> std::io::Result<usize> {
928946
let mut pending = len;
929947
while pending > 0 {
930-
let ret = unsafe {
931-
libc::copy_file_range(
932-
in_fd,
933-
&raw mut in_off,
934-
out_fd,
935-
std::ptr::null_mut(), // Use and update output offset
936-
pending,
937-
0,
938-
)
939-
};
940-
if ret < 0 {
941-
let err = io::Error::last_os_error();
942-
return match err.raw_os_error() {
943-
Some(libc::ENOSYS) | Some(libc::EOPNOTSUPP) | Some(libc::EXDEV) => {
944-
// Fallback to write(2).
945-
reliable_write(out_fd, in_ptr, pending)
946-
}
947-
_ => Err(err),
948-
};
949-
} else if ret == 0 {
950-
// EOF reached
951-
break;
948+
let mut in_off_u64 = in_off as u64;
949+
let result: std::io::Result<usize> =
950+
rustix_copy_file_range(in_fd, Some(&mut in_off_u64), out_fd, None, pending)
951+
.map_err(std::io::Error::from);
952+
953+
match result {
954+
Ok(0) => break,
955+
Ok(ret) => {
956+
pending -= ret;
957+
in_off = in_off_u64 as libc::off_t;
958+
}
959+
Err(err) => {
960+
return match err.raw_os_error() {
961+
Some(libc::ENOSYS) | Some(libc::EOPNOTSUPP) | Some(libc::EXDEV) => {
962+
// Fallback to write(2).
963+
reliable_write(out_fd.as_raw_fd(), in_ptr, pending)
964+
}
965+
_ => Err(err),
966+
};
967+
}
952968
}
953-
pending -= ret as usize;
954969
}
955970
Ok(len)
956971
}
@@ -962,19 +977,15 @@ fn reliable_copy_file_range(
962977
#[cfg(all(target_os = "linux", target_env = "gnu"))]
963978
fn aligned_copy_file_range(
964979
mut in_ptr: *const u8,
965-
in_fd: i32,
980+
in_fd: BorrowedFd<'_>,
966981
mut in_off: libc::off_t,
967-
out_fd: i32,
982+
out_fd: BorrowedFd<'_>,
968983
len: usize,
969984
block_size: usize,
970985
cover: WriteRange,
971986
) -> std::io::Result<usize> {
972-
// 1. Get current output offset.
973-
let res = unsafe { libc::lseek(out_fd, 0, libc::SEEK_CUR) as i64 };
974-
if res < 0 {
975-
return Err(std::io::Error::last_os_error());
976-
}
977-
let out_off = res as usize;
987+
// Get current output offset.
988+
let out_off = rustix::fs::tell(out_fd)? as usize;
978989
let mut pending = len;
979990

980991
// Obtain head alignment.
@@ -991,7 +1002,7 @@ fn aligned_copy_file_range(
9911002
if head_align > 0 {
9921003
// Align the two files on a block boundary.
9931004
let head_len = head_align.min(pending);
994-
reliable_write(out_fd, in_ptr, head_len)?;
1005+
reliable_write(out_fd.as_raw_fd(), in_ptr, head_len)?;
9951006
in_ptr = unsafe { in_ptr.add(head_len) };
9961007
in_off += head_len as i64;
9971008
pending -= head_len;
@@ -1005,7 +1016,7 @@ fn aligned_copy_file_range(
10051016
// Copy tail if needed.
10061017
if pending > 0 && cover == WriteRange::Complete {
10071018
in_ptr = unsafe { in_ptr.add(aligned_len) };
1008-
pending -= reliable_write(out_fd, in_ptr, pending)?;
1019+
pending -= reliable_write(out_fd.as_raw_fd(), in_ptr, pending)?;
10091020
}
10101021

10111022
Ok(len - pending)
@@ -1038,6 +1049,8 @@ mod tests {
10381049
#[cfg(all(target_os = "linux", target_env = "gnu"))]
10391050
use std::io::{self, Write};
10401051
use std::io::{Seek, SeekFrom};
1052+
#[cfg(target_os = "linux")]
1053+
use std::os::unix::io::AsFd;
10411054
use tempfile::NamedTempFile;
10421055
use tempfile::tempfile;
10431056

@@ -1773,8 +1786,8 @@ mod tests {
17731786
infile.write_all(data).unwrap();
17741787
infile.rewind().unwrap();
17751788

1776-
let in_fd = infile.as_raw_fd();
1777-
let out_fd = outfile.as_raw_fd();
1789+
let in_fd = infile.as_fd();
1790+
let out_fd = outfile.as_fd();
17781791
let in_ptr = data.as_ptr();
17791792

17801793
// Copy with block size 4, cover = Complete
@@ -1809,8 +1822,8 @@ mod tests {
18091822
infile.write_all(data).unwrap();
18101823
infile.rewind().unwrap();
18111824

1812-
let in_fd = infile.as_raw_fd();
1813-
let out_fd = outfile.as_raw_fd();
1825+
let in_fd = infile.as_fd();
1826+
let out_fd = outfile.as_fd();
18141827
let in_ptr = data.as_ptr();
18151828

18161829
// Copy with block size 4, cover = Blocks
@@ -1828,6 +1841,94 @@ mod tests {
18281841
assert_eq!(&buf, b"abcdefgh");
18291842
}
18301843

1844+
#[test]
1845+
#[cfg(target_os = "linux")]
1846+
fn test_aligned_copy_respects_existing_out_offset() {
1847+
let mut infile = tempfile().unwrap();
1848+
let mut outfile = tempfile().unwrap();
1849+
1850+
// Write a prefix into the output file so tell() returns a non-zero offset.
1851+
let prefix = b"HEADER";
1852+
outfile.write_all(prefix).unwrap();
1853+
outfile.flush().unwrap();
1854+
1855+
// Input data = 12 bytes (3 blocks of 4 when block_size=4).
1856+
let data = b"abcdefghijkl";
1857+
infile.write_all(data).unwrap();
1858+
infile.rewind().unwrap();
1859+
1860+
let in_fd = infile.as_fd();
1861+
let out_fd = outfile.as_fd();
1862+
let in_ptr = data.as_ptr();
1863+
1864+
let copied = aligned_copy_file_range(
1865+
in_ptr,
1866+
in_fd,
1867+
0,
1868+
out_fd,
1869+
data.len(),
1870+
4,
1871+
WriteRange::Complete,
1872+
)
1873+
.unwrap();
1874+
1875+
assert_eq!(copied, data.len());
1876+
1877+
outfile.rewind().unwrap();
1878+
let mut buf = Vec::new();
1879+
outfile.read_to_end(&mut buf).unwrap();
1880+
1881+
assert_eq!(&buf, b"HEADERabcdefghijkl");
1882+
}
1883+
1884+
#[test]
1885+
#[cfg(target_os = "linux")]
1886+
fn test_reliable_copy_file_range_rustix_path() {
1887+
let mut infile = tempfile().unwrap();
1888+
let mut outfile = tempfile().unwrap();
1889+
1890+
let data = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1891+
infile.write_all(data).unwrap();
1892+
infile.rewind().unwrap();
1893+
1894+
let in_fd = infile.as_fd();
1895+
let out_fd = outfile.as_fd();
1896+
let in_ptr = data.as_ptr();
1897+
1898+
// Copy a subrange starting at offset 10.
1899+
let copied = reliable_copy_file_range(in_ptr, in_fd, 10, out_fd, 16).unwrap();
1900+
1901+
assert_eq!(copied, 16);
1902+
1903+
outfile.rewind().unwrap();
1904+
let mut buf = Vec::new();
1905+
outfile.read_to_end(&mut buf).unwrap();
1906+
assert_eq!(&buf, b"ABCDEFGHIJKLMNOP");
1907+
1908+
// Now verify that copying into a file with existing data appends correctly.
1909+
let mut infile2 = tempfile().unwrap();
1910+
let mut outfile2 = tempfile().unwrap();
1911+
let input_data = b"abcdefghijklmnopqrstuvwxyz";
1912+
infile2.write_all(input_data).unwrap();
1913+
infile2.rewind().unwrap();
1914+
1915+
outfile2.write_all(b"PRE:").unwrap();
1916+
outfile2.flush().unwrap();
1917+
1918+
let in_fd2 = infile2.as_fd();
1919+
let out_fd2 = outfile2.as_fd();
1920+
let in_ptr2 = input_data.as_ptr();
1921+
1922+
let copied2 = reliable_copy_file_range(in_ptr2, in_fd2, 5, out_fd2, 10).unwrap();
1923+
1924+
assert_eq!(copied2, 10);
1925+
1926+
outfile2.rewind().unwrap();
1927+
let mut buf2 = Vec::new();
1928+
outfile2.read_to_end(&mut buf2).unwrap();
1929+
assert_eq!(&buf2, b"PRE:fghijklmno");
1930+
}
1931+
18311932
///////////////////////////////
18321933
// Unit tests for write_chunk()
18331934
///////////////////////////////

0 commit comments

Comments
 (0)