mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
uucore/buf_copy: Improve the uucore integration (#6983)
--------- Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
parent
067b2964c6
commit
edf8be5e08
6 changed files with 450 additions and 288 deletions
|
@ -6,6 +6,8 @@
|
|||
|
||||
#[cfg(feature = "backup-control")]
|
||||
pub mod backup_control;
|
||||
#[cfg(feature = "buf-copy")]
|
||||
pub mod buf_copy;
|
||||
#[cfg(feature = "checksum")]
|
||||
pub mod checksum;
|
||||
#[cfg(feature = "colors")]
|
||||
|
@ -39,8 +41,6 @@ pub mod version_cmp;
|
|||
pub mod mode;
|
||||
|
||||
// ** unix-only
|
||||
#[cfg(all(any(target_os = "linux", target_os = "android"), feature = "buf-copy"))]
|
||||
pub mod buf_copy;
|
||||
#[cfg(all(unix, feature = "entries"))]
|
||||
pub mod entries;
|
||||
#[cfg(all(unix, feature = "perms"))]
|
||||
|
|
|
@ -9,283 +9,51 @@
|
|||
//! used by utilities to work around the limitations of Rust's `fs::copy` which
|
||||
//! does not handle copying special files (e.g pipes, character/block devices).
|
||||
|
||||
use crate::error::{UError, UResult};
|
||||
use nix::unistd;
|
||||
use std::fs::File;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
os::{
|
||||
fd::AsFd,
|
||||
unix::io::{AsRawFd, RawFd},
|
||||
},
|
||||
};
|
||||
pub mod common;
|
||||
|
||||
use nix::{errno::Errno, libc::S_IFIFO, sys::stat::fstat};
|
||||
|
||||
use super::pipes::{pipe, splice, splice_exact, vmsplice};
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Error types used by buffer-copying functions from the `buf_copy` module.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(io::Error),
|
||||
WriteError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::WriteError(msg) => write!(f, "splice() write error: {}", msg),
|
||||
Error::Io(err) => write!(f, "I/O error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl UError for Error {
|
||||
fn code(&self) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn usage(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to determine whether a given handle (such as a file) is a pipe or not.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `out` - path of handle
|
||||
///
|
||||
/// # Returns
|
||||
/// A `bool` indicating whether the given handle is a pipe or not.
|
||||
#[inline]
|
||||
#[cfg(unix)]
|
||||
pub fn is_pipe<P>(path: &P) -> Result<bool>
|
||||
where
|
||||
P: AsRawFd,
|
||||
{
|
||||
Ok(fstat(path.as_raw_fd())?.st_mode as nix::libc::mode_t & S_IFIFO != 0)
|
||||
}
|
||||
|
||||
const SPLICE_SIZE: usize = 1024 * 128;
|
||||
const BUF_SIZE: usize = 1024 * 16;
|
||||
|
||||
/// Copy data from `Read` implementor `source` into a `Write` implementor
|
||||
/// `dest`. This works by reading a chunk of data from `source` and writing the
|
||||
/// data to `dest` in a loop.
|
||||
///
|
||||
/// This function uses the Linux-specific `splice` call when possible which does
|
||||
/// not use any intermediate user-space buffer. It falls backs to
|
||||
/// `std::io::copy` under other platforms or when the call fails and is still
|
||||
/// recoverable.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source` - `Read` implementor to copy data from.
|
||||
/// * `dest` - `Write` implementor to copy data to.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Result of operation and bytes successfully written (as a `u64`) when
|
||||
/// operation is successful.
|
||||
pub fn copy_stream<R, S>(src: &mut R, dest: &mut S) -> UResult<u64>
|
||||
where
|
||||
R: Read + AsFd + AsRawFd,
|
||||
S: Write + AsFd + AsRawFd,
|
||||
{
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
// If we're on Linux or Android, try to use the splice() system call
|
||||
// for faster writing. If it works, we're done.
|
||||
let result = splice_write(src, &dest.as_fd())?;
|
||||
if !result.1 {
|
||||
return Ok(result.0);
|
||||
}
|
||||
}
|
||||
// If we're not on Linux or Android, or the splice() call failed,
|
||||
// fall back on slower writing.
|
||||
let result = std::io::copy(src, dest)?;
|
||||
|
||||
// If the splice() call failed and there has been some data written to
|
||||
// stdout via while loop above AND there will be second splice() call
|
||||
// that will succeed, data pushed through splice will be output before
|
||||
// the data buffered in stdout.lock. Therefore additional explicit flush
|
||||
// is required here.
|
||||
dest.flush()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Write from source `handle` into destination `write_fd` using Linux-specific
|
||||
/// `splice` system call.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `source` - source handle
|
||||
/// - `dest` - destination handle
|
||||
#[inline]
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn splice_write<R, S>(source: &R, dest: &S) -> UResult<(u64, bool)>
|
||||
where
|
||||
R: Read + AsFd + AsRawFd,
|
||||
S: AsRawFd + AsFd,
|
||||
{
|
||||
let (pipe_rd, pipe_wr) = pipe()?;
|
||||
let mut bytes: u64 = 0;
|
||||
|
||||
loop {
|
||||
match splice(&source, &pipe_wr, SPLICE_SIZE) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
return Ok((bytes, false));
|
||||
}
|
||||
if splice_exact(&pipe_rd, dest, n).is_err() {
|
||||
// If the first splice manages to copy to the intermediate
|
||||
// pipe, but the second splice to stdout fails for some reason
|
||||
// we can recover by copying the data that we have from the
|
||||
// intermediate pipe to stdout using normal read/write. Then
|
||||
// we tell the caller to fall back.
|
||||
copy_exact(pipe_rd.as_raw_fd(), dest, n)?;
|
||||
return Ok((bytes, true));
|
||||
}
|
||||
|
||||
bytes += n as u64;
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok((bytes, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Move exactly `num_bytes` bytes from `read_fd` to `write_fd` using the `read`
|
||||
/// and `write` calls.
|
||||
fn copy_exact(read_fd: RawFd, write_fd: &impl AsFd, num_bytes: usize) -> std::io::Result<usize> {
|
||||
let mut left = num_bytes;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let mut written = 0;
|
||||
while left > 0 {
|
||||
let read = unistd::read(read_fd, &mut buf)?;
|
||||
assert_ne!(read, 0, "unexpected end of pipe");
|
||||
while written < read {
|
||||
let n = unistd::write(write_fd, &buf[written..read])?;
|
||||
written += n;
|
||||
}
|
||||
left -= read;
|
||||
}
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
/// Write input `bytes` to a file descriptor. This uses the Linux-specific
|
||||
/// `vmsplice()` call to write into a file descriptor directly, which only works
|
||||
/// if the destination is a pipe.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bytes` - data to be written
|
||||
/// * `dest` - destination handle
|
||||
///
|
||||
/// # Returns
|
||||
/// When write succeeds, the amount of bytes written is returned as a
|
||||
/// `u64`. The `bool` indicates if we need to fall back to normal copying or
|
||||
/// not. `true` means we need to fall back, `false` means we don't have to.
|
||||
///
|
||||
/// A `UError` error is returned when the operation is not supported or when an
|
||||
/// I/O error occurs.
|
||||
pub mod linux;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn splice_data_to_pipe<T>(bytes: &[u8], dest: &T) -> UResult<(u64, bool)>
|
||||
where
|
||||
T: AsRawFd + AsFd,
|
||||
{
|
||||
let mut n_bytes: u64 = 0;
|
||||
let mut bytes = bytes;
|
||||
while !bytes.is_empty() {
|
||||
let len = match vmsplice(dest, bytes) {
|
||||
Ok(n) => n,
|
||||
// The maybe_unsupported call below may emit an error, when the
|
||||
// error is considered as unrecoverable error (ones that won't make
|
||||
// us fall back to other method)
|
||||
Err(e) => return Ok(maybe_unsupported(e)?),
|
||||
};
|
||||
bytes = &bytes[len..];
|
||||
n_bytes += len as u64;
|
||||
}
|
||||
Ok((n_bytes, false))
|
||||
}
|
||||
pub use linux::*;
|
||||
|
||||
/// Write input `bytes` to a handle using a temporary pipe. A `vmsplice()` call
|
||||
/// is issued to write to the temporary pipe, which then gets written to the
|
||||
/// final destination using `splice()`.
|
||||
///
|
||||
/// # Arguments * `bytes` - data to be written * `dest` - destination handle
|
||||
///
|
||||
/// # Returns When write succeeds, the amount of bytes written is returned as a
|
||||
/// `u64`. The `bool` indicates if we need to fall back to normal copying or
|
||||
/// not. `true` means we need to fall back, `false` means we don't have to.
|
||||
///
|
||||
/// A `UError` error is returned when the operation is not supported or when an
|
||||
/// I/O error occurs.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn splice_data_to_fd<T: AsFd>(
|
||||
bytes: &[u8],
|
||||
read_pipe: &File,
|
||||
write_pipe: &File,
|
||||
dest: &T,
|
||||
) -> UResult<(u64, bool)> {
|
||||
loop {
|
||||
let mut bytes = bytes;
|
||||
while !bytes.is_empty() {
|
||||
let len = match vmsplice(&write_pipe, bytes) {
|
||||
Ok(n) => n,
|
||||
Err(e) => return Ok(maybe_unsupported(e)?),
|
||||
};
|
||||
if let Err(e) = splice_exact(&read_pipe, dest, len) {
|
||||
return Ok(maybe_unsupported(e)?);
|
||||
}
|
||||
bytes = &bytes[len..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversion from a `nix::Error` into our `Error` which implements `UError`.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
impl From<nix::Error> for Error {
|
||||
fn from(error: nix::Error) -> Self {
|
||||
Self::Io(io::Error::from_raw_os_error(error as i32))
|
||||
}
|
||||
}
|
||||
|
||||
/// Several error values from `nix::Error` (`EINVAL`, `ENOSYS`, and `EBADF`) get
|
||||
/// treated as errors indicating that the `splice` call is not available, i.e we
|
||||
/// can still recover from the error. Thus, return the final result of the call
|
||||
/// as `Result` and indicate that we have to fall back using other write method.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `error` - the `nix::Error` received
|
||||
///
|
||||
/// # Returns
|
||||
/// Result with tuple containing a `u64` `0` indicating that no data had been
|
||||
/// written and a `true` indicating we have to fall back, if error is still
|
||||
/// recoverable. Returns an `Error` implementing `UError` otherwise.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn maybe_unsupported(error: nix::Error) -> Result<(u64, bool)> {
|
||||
match error {
|
||||
Errno::EINVAL | Errno::ENOSYS | Errno::EBADF => Ok((0, true)),
|
||||
_ => Err(error.into()),
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub mod other;
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub use other::copy_stream;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::File;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::pipes;
|
||||
#[cfg(unix)]
|
||||
use {
|
||||
crate::pipes,
|
||||
std::fs::OpenOptions,
|
||||
std::{
|
||||
io::{Seek, SeekFrom},
|
||||
thread,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use {nix::unistd, std::os::fd::AsRawFd};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[cfg(unix)]
|
||||
fn new_temp_file() -> File {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
File::create(temp_dir.path().join("file.txt")).unwrap()
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(temp_dir.path().join("file.txt"))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_file_is_pipe() {
|
||||
let temp_file = new_temp_file();
|
||||
|
@ -296,21 +64,26 @@ mod tests {
|
|||
assert!(!is_pipe(&temp_file).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_valid_splice_errs() {
|
||||
let err = nix::Error::from(Errno::EINVAL);
|
||||
use nix::errno::Errno;
|
||||
use nix::Error;
|
||||
|
||||
let err = Error::from(Errno::EINVAL);
|
||||
assert_eq!(maybe_unsupported(err).unwrap(), (0, true));
|
||||
|
||||
let err = nix::Error::from(Errno::ENOSYS);
|
||||
let err = Error::from(Errno::ENOSYS);
|
||||
assert_eq!(maybe_unsupported(err).unwrap(), (0, true));
|
||||
|
||||
let err = nix::Error::from(Errno::EBADF);
|
||||
let err = Error::from(Errno::EBADF);
|
||||
assert_eq!(maybe_unsupported(err).unwrap(), (0, true));
|
||||
|
||||
let err = nix::Error::from(Errno::EPERM);
|
||||
let err = Error::from(Errno::EPERM);
|
||||
assert!(maybe_unsupported(err).is_err());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_splice_data_to_pipe() {
|
||||
let (pipe_read, pipe_write) = pipes::pipe().unwrap();
|
||||
|
@ -322,18 +95,26 @@ mod tests {
|
|||
assert_eq!(bytes as usize, data.len());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_splice_data_to_file() {
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
let mut temp_file = new_temp_file();
|
||||
let (pipe_read, pipe_write) = pipes::pipe().unwrap();
|
||||
let data = b"Hello, world!";
|
||||
let (bytes, _) = splice_data_to_fd(data, &pipe_read, &pipe_write, &temp_file).unwrap();
|
||||
let mut buf = [0; 1024];
|
||||
let n = temp_file.read(&mut buf).unwrap();
|
||||
assert_eq!(&buf[..n], data);
|
||||
assert_eq!(bytes as usize, data.len());
|
||||
|
||||
// We would have been at the end already, so seek again to the start.
|
||||
temp_file.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
temp_file.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(buf, data);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_copy_exact() {
|
||||
let (mut pipe_read, mut pipe_write) = pipes::pipe().unwrap();
|
||||
|
@ -348,26 +129,79 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_copy_stream() {
|
||||
let mut dest_file = new_temp_file();
|
||||
|
||||
let (mut pipe_read, mut pipe_write) = pipes::pipe().unwrap();
|
||||
let data = b"Hello, world!";
|
||||
let n = pipe_write.write(data).unwrap();
|
||||
assert_eq!(n, data.len());
|
||||
let mut buf = [0; 1024];
|
||||
let n = copy_stream(&mut pipe_read, &mut pipe_write).unwrap();
|
||||
let n2 = pipe_read.read(&mut buf).unwrap();
|
||||
assert_eq!(n as usize, n2);
|
||||
assert_eq!(&buf[..n as usize], data);
|
||||
let thread = thread::spawn(move || {
|
||||
pipe_write.write_all(data).unwrap();
|
||||
});
|
||||
let result = copy_stream(&mut pipe_read, &mut dest_file).unwrap();
|
||||
thread.join().unwrap();
|
||||
assert!(result == data.len() as u64);
|
||||
|
||||
// We would have been at the end already, so seek again to the start.
|
||||
dest_file.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
dest_file.read_to_end(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(buf, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_write() {
|
||||
let (mut pipe_read, pipe_write) = pipes::pipe().unwrap();
|
||||
#[cfg(not(unix))]
|
||||
// Test for non-unix platforms. We use regular files instead.
|
||||
fn test_copy_stream() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let src_path = temp_dir.path().join("src.txt");
|
||||
let dest_path = temp_dir.path().join("dest.txt");
|
||||
|
||||
let mut src_file = File::create(&src_path).unwrap();
|
||||
let mut dest_file = File::create(&dest_path).unwrap();
|
||||
|
||||
let data = b"Hello, world!";
|
||||
let (bytes, _) = splice_write(&pipe_read, &pipe_write).unwrap();
|
||||
let mut buf = [0; 1024];
|
||||
let n = pipe_read.read(&mut buf).unwrap();
|
||||
assert_eq!(&buf[..n], data);
|
||||
assert_eq!(bytes as usize, data.len());
|
||||
src_file.write_all(data).unwrap();
|
||||
src_file.sync_all().unwrap();
|
||||
|
||||
let mut src_file = File::open(&src_path).unwrap();
|
||||
let bytes_copied = copy_stream(&mut src_file, &mut dest_file).unwrap();
|
||||
|
||||
let mut dest_file = File::open(&dest_path).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
dest_file.read_to_end(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(bytes_copied as usize, data.len());
|
||||
assert_eq!(buf, data);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[test]
|
||||
fn test_splice_write() {
|
||||
use std::{
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
thread,
|
||||
};
|
||||
|
||||
let (pipe_read, mut pipe_write) = pipes::pipe().unwrap();
|
||||
let mut dest_file = new_temp_file();
|
||||
let data = b"Hello, world!";
|
||||
let thread = thread::spawn(move || {
|
||||
pipe_write.write_all(data).unwrap();
|
||||
});
|
||||
let (bytes, _) = splice_write(&pipe_read, &dest_file).unwrap();
|
||||
thread.join().unwrap();
|
||||
|
||||
assert!(bytes == data.len() as u64);
|
||||
|
||||
// We would have been at the end already, so seek again to the start.
|
||||
dest_file.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
dest_file.read_to_end(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(buf, data);
|
||||
}
|
||||
}
|
||||
|
|
34
src/uucore/src/lib/features/buf_copy/common.rs
Normal file
34
src/uucore/src/lib/features/buf_copy/common.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// This file is part of the uutils coreutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use crate::error::UError;
|
||||
|
||||
/// Error types used by buffer-copying functions from the `buf_copy` module.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(std::io::Error),
|
||||
WriteError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::WriteError(msg) => write!(f, "splice() write error: {}", msg),
|
||||
Error::Io(err) => write!(f, "I/O error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl UError for Error {
|
||||
fn code(&self) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn usage(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
264
src/uucore/src/lib/features/buf_copy/linux.rs
Normal file
264
src/uucore/src/lib/features/buf_copy/linux.rs
Normal file
|
@ -0,0 +1,264 @@
|
|||
// This file is part of the uutils coreutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use nix::sys::stat::fstat;
|
||||
use nix::{errno::Errno, libc::S_IFIFO};
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Buffer-based copying utilities for Linux and Android.
|
||||
use crate::{
|
||||
error::UResult,
|
||||
pipes::{pipe, splice, splice_exact, vmsplice},
|
||||
};
|
||||
|
||||
/// Buffer-based copying utilities for unix (excluding Linux).
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
os::fd::{AsFd, AsRawFd, RawFd},
|
||||
};
|
||||
|
||||
use super::common::Error;
|
||||
|
||||
/// A readable file descriptor.
|
||||
pub trait FdReadable: Read + AsRawFd + AsFd {}
|
||||
|
||||
impl<T> FdReadable for T where T: Read + AsFd + AsRawFd {}
|
||||
|
||||
/// A writable file descriptor.
|
||||
pub trait FdWritable: Write + AsFd + AsRawFd {}
|
||||
|
||||
impl<T> FdWritable for T where T: Write + AsFd + AsRawFd {}
|
||||
|
||||
const SPLICE_SIZE: usize = 1024 * 128;
|
||||
const BUF_SIZE: usize = 1024 * 16;
|
||||
|
||||
/// Conversion from a `nix::Error` into our `Error` which implements `UError`.
|
||||
impl From<nix::Error> for Error {
|
||||
fn from(error: nix::Error) -> Self {
|
||||
Self::Io(std::io::Error::from_raw_os_error(error as i32))
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy data from `Read` implementor `source` into a `Write` implementor
|
||||
/// `dest`. This works by reading a chunk of data from `source` and writing the
|
||||
/// data to `dest` in a loop.
|
||||
///
|
||||
/// This function uses the Linux-specific `splice` call when possible which does
|
||||
/// not use any intermediate user-space buffer. It falls backs to
|
||||
/// `std::io::copy` when the call fails and is still recoverable.
|
||||
///
|
||||
/// # Arguments * `source` - `Read` implementor to copy data from. * `dest` -
|
||||
/// `Write` implementor to copy data to.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Result of operation and bytes successfully written (as a `u64`) when
|
||||
/// operation is successful.
|
||||
///
|
||||
|
||||
pub fn copy_stream<R, S>(src: &mut R, dest: &mut S) -> UResult<u64>
|
||||
where
|
||||
R: Read + AsFd + AsRawFd,
|
||||
S: Write + AsFd + AsRawFd,
|
||||
{
|
||||
// If we're on Linux or Android, try to use the splice() system call
|
||||
// for faster writing. If it works, we're done.
|
||||
let result = splice_write(src, &dest.as_fd())?;
|
||||
if !result.1 {
|
||||
return Ok(result.0);
|
||||
}
|
||||
|
||||
// If the splice() call failed, fall back on slower writing.
|
||||
let result = std::io::copy(src, dest)?;
|
||||
|
||||
// If the splice() call failed and there has been some data written to
|
||||
// stdout via while loop above AND there will be second splice() call
|
||||
// that will succeed, data pushed through splice will be output before
|
||||
// the data buffered in stdout.lock. Therefore additional explicit flush
|
||||
// is required here.
|
||||
dest.flush()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Write from source `handle` into destination `write_fd` using Linux-specific
|
||||
/// `splice` system call.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `source` - source handle
|
||||
/// - `dest` - destination handle
|
||||
#[inline]
|
||||
pub(crate) fn splice_write<R, S>(source: &R, dest: &S) -> UResult<(u64, bool)>
|
||||
where
|
||||
R: Read + AsFd + AsRawFd,
|
||||
S: AsRawFd + AsFd,
|
||||
{
|
||||
let (pipe_rd, pipe_wr) = pipe()?;
|
||||
let mut bytes: u64 = 0;
|
||||
|
||||
loop {
|
||||
match splice(&source, &pipe_wr, SPLICE_SIZE) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
return Ok((bytes, false));
|
||||
}
|
||||
if splice_exact(&pipe_rd, dest, n).is_err() {
|
||||
// If the first splice manages to copy to the intermediate
|
||||
// pipe, but the second splice to stdout fails for some reason
|
||||
// we can recover by copying the data that we have from the
|
||||
// intermediate pipe to stdout using normal read/write. Then
|
||||
// we tell the caller to fall back.
|
||||
copy_exact(pipe_rd.as_raw_fd(), dest, n)?;
|
||||
return Ok((bytes, true));
|
||||
}
|
||||
|
||||
bytes += n as u64;
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok((bytes, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Move exactly `num_bytes` bytes from `read_fd` to `write_fd` using the `read`
|
||||
/// and `write` calls.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub(crate) fn copy_exact(
|
||||
read_fd: RawFd,
|
||||
write_fd: &impl AsFd,
|
||||
num_bytes: usize,
|
||||
) -> std::io::Result<usize> {
|
||||
use nix::unistd;
|
||||
|
||||
let mut left = num_bytes;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let mut written = 0;
|
||||
while left > 0 {
|
||||
let read = unistd::read(read_fd, &mut buf)?;
|
||||
assert_ne!(read, 0, "unexpected end of pipe");
|
||||
while written < read {
|
||||
let n = unistd::write(write_fd, &buf[written..read])?;
|
||||
written += n;
|
||||
}
|
||||
left -= read;
|
||||
}
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
// The generalization of this function (and other splice_data functions) is not trivial as most
|
||||
// utilities will just write data finitely. However, `yes`, which is the sole crate using these
|
||||
// functions as of now, continuously loops the data write. Coupling the `is_pipe` check together
|
||||
// with the data write logic means that the check has to be done for every single write, which adds
|
||||
// unnecessary overhead.
|
||||
//
|
||||
/// Helper function to determine whether a given handle (such as a file) is a pipe or not. Can be
|
||||
/// used to determine whether to use the `splice_data_to_pipe` or the `splice_data_to_fd` function.
|
||||
/// This function is available exclusively to Linux and Android as it is meant to be used at the
|
||||
/// scope of splice operations.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `out` - path of handle
|
||||
///
|
||||
/// # Returns
|
||||
/// A `bool` indicating whether the given handle is a pipe or not.
|
||||
#[inline]
|
||||
pub fn is_pipe<P>(path: &P) -> Result<bool>
|
||||
where
|
||||
P: AsRawFd,
|
||||
{
|
||||
Ok(fstat(path.as_raw_fd())?.st_mode as nix::libc::mode_t & S_IFIFO != 0)
|
||||
}
|
||||
|
||||
/// Write input `bytes` to a handle using a temporary pipe. A `vmsplice()` call
|
||||
/// is issued to write to the temporary pipe, which then gets written to the
|
||||
/// final destination using `splice()`.
|
||||
///
|
||||
/// # Arguments * `bytes` - data to be written * `dest` - destination handle
|
||||
///
|
||||
/// # Returns When write succeeds, the amount of bytes written is returned as a
|
||||
/// `u64`. The `bool` indicates if we need to fall back to normal copying or
|
||||
/// not. `true` means we need to fall back, `false` means we don't have to.
|
||||
///
|
||||
/// A `UError` error is returned when the operation is not supported or when an
|
||||
/// I/O error occurs.
|
||||
pub fn splice_data_to_fd<T: AsFd>(
|
||||
bytes: &[u8],
|
||||
read_pipe: &File,
|
||||
write_pipe: &File,
|
||||
dest: &T,
|
||||
) -> UResult<(u64, bool)> {
|
||||
let mut n_bytes: u64 = 0;
|
||||
let mut bytes = bytes;
|
||||
while !bytes.is_empty() {
|
||||
let len = match vmsplice(&write_pipe, bytes) {
|
||||
Ok(n) => n,
|
||||
Err(e) => return Ok(maybe_unsupported(e)?),
|
||||
};
|
||||
if let Err(e) = splice_exact(&read_pipe, dest, len) {
|
||||
return Ok(maybe_unsupported(e)?);
|
||||
}
|
||||
bytes = &bytes[len..];
|
||||
n_bytes += len as u64;
|
||||
}
|
||||
Ok((n_bytes, false))
|
||||
}
|
||||
|
||||
/// Write input `bytes` to a file descriptor. This uses the Linux-specific
|
||||
/// `vmsplice()` call to write into a file descriptor directly, which only works
|
||||
/// if the destination is a pipe.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bytes` - data to be written
|
||||
/// * `dest` - destination handle
|
||||
///
|
||||
/// # Returns
|
||||
/// When write succeeds, the amount of bytes written is returned as a
|
||||
/// `u64`. The `bool` indicates if we need to fall back to normal copying or
|
||||
/// not. `true` means we need to fall back, `false` means we don't have to.
|
||||
///
|
||||
/// A `UError` error is returned when the operation is not supported or when an
|
||||
/// I/O error occurs.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn splice_data_to_pipe<T>(bytes: &[u8], dest: &T) -> UResult<(u64, bool)>
|
||||
where
|
||||
T: AsRawFd + AsFd,
|
||||
{
|
||||
let mut n_bytes: u64 = 0;
|
||||
let mut bytes = bytes;
|
||||
while !bytes.is_empty() {
|
||||
let len = match vmsplice(dest, bytes) {
|
||||
Ok(n) => n,
|
||||
// The maybe_unsupported call below may emit an error, when the
|
||||
// error is considered as unrecoverable error (ones that won't make
|
||||
// us fall back to other method)
|
||||
Err(e) => return Ok(maybe_unsupported(e)?),
|
||||
};
|
||||
bytes = &bytes[len..];
|
||||
n_bytes += len as u64;
|
||||
}
|
||||
Ok((n_bytes, false))
|
||||
}
|
||||
|
||||
/// Several error values from `nix::Error` (`EINVAL`, `ENOSYS`, and `EBADF`) get
|
||||
/// treated as errors indicating that the `splice` call is not available, i.e we
|
||||
/// can still recover from the error. Thus, return the final result of the call
|
||||
/// as `Result` and indicate that we have to fall back using other write method.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `error` - the `nix::Error` received
|
||||
///
|
||||
/// # Returns
|
||||
/// Result with tuple containing a `u64` `0` indicating that no data had been
|
||||
/// written and a `true` indicating we have to fall back, if error is still
|
||||
/// recoverable. Returns an `Error` implementing `UError` otherwise.
|
||||
pub(crate) fn maybe_unsupported(error: nix::Error) -> Result<(u64, bool)> {
|
||||
match error {
|
||||
Errno::EINVAL | Errno::ENOSYS | Errno::EBADF => Ok((0, true)),
|
||||
_ => Err(error.into()),
|
||||
}
|
||||
}
|
30
src/uucore/src/lib/features/buf_copy/other.rs
Normal file
30
src/uucore/src/lib/features/buf_copy/other.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
// This file is part of the uutils coreutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::error::UResult;
|
||||
|
||||
/// Copy data from `Read` implementor `source` into a `Write` implementor
|
||||
/// `dest`. This works by reading a chunk of data from `source` and writing the
|
||||
/// data to `dest` in a loop, using std::io::copy. This is implemented for
|
||||
/// non-Linux platforms.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source` - `Read` implementor to copy data from.
|
||||
/// * `dest` - `Write` implementor to copy data to.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Result of operation and bytes successfully written (as a `u64`) when
|
||||
/// operation is successful.
|
||||
pub fn copy_stream<R, S>(src: &mut R, dest: &mut S) -> UResult<u64>
|
||||
where
|
||||
R: Read,
|
||||
S: Write,
|
||||
{
|
||||
let result = std::io::copy(src, dest)?;
|
||||
Ok(result)
|
||||
}
|
|
@ -40,6 +40,8 @@ pub use crate::parser::shortcut_value_parser;
|
|||
// * feature-gated modules
|
||||
#[cfg(feature = "backup-control")]
|
||||
pub use crate::features::backup_control;
|
||||
#[cfg(feature = "buf-copy")]
|
||||
pub use crate::features::buf_copy;
|
||||
#[cfg(feature = "checksum")]
|
||||
pub use crate::features::checksum;
|
||||
#[cfg(feature = "colors")]
|
||||
|
@ -70,8 +72,6 @@ pub use crate::features::version_cmp;
|
|||
#[cfg(all(not(windows), feature = "mode"))]
|
||||
pub use crate::features::mode;
|
||||
// ** unix-only
|
||||
#[cfg(all(any(target_os = "linux", target_os = "android"), feature = "buf-copy"))]
|
||||
pub use crate::features::buf_copy;
|
||||
#[cfg(all(unix, feature = "entries"))]
|
||||
pub use crate::features::entries;
|
||||
#[cfg(all(unix, feature = "perms"))]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue