From 2d0f59c51d7b31feb80cb3039d27d04580961c6f Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 5 Sep 2022 17:10:43 -0400 Subject: [PATCH 1/2] cp: move copy_on_write funcs to platform module Move the copy-on-write functions for `cp` to their own module. This provides a layer of indirection so that the `cp.rs` module need only use `platform::copy_on_write()`, and the `platform` module is responsible for providing the appropriate implementation for the current platform. This commit does not change the behavior of the code, just its organization. --- src/uu/cp/src/cp.rs | 212 +------------------------------- src/uu/cp/src/platform/linux.rs | 110 +++++++++++++++++ src/uu/cp/src/platform/macos.rs | 74 +++++++++++ src/uu/cp/src/platform/mod.rs | 18 +++ src/uu/cp/src/platform/other.rs | 33 +++++ 5 files changed, 239 insertions(+), 208 deletions(-) create mode 100644 src/uu/cp/src/platform/linux.rs create mode 100644 src/uu/cp/src/platform/macos.rs create mode 100644 src/uu/cp/src/platform/mod.rs create mode 100644 src/uu/cp/src/platform/other.rs diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index ef8fb93ec..e97ce514e 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -22,15 +22,11 @@ use std::env; #[cfg(not(windows))] use std::ffi::CString; use std::fs::{self, File, OpenOptions}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::io::Read; use std::io::{self, stderr, stdin, Write}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, PermissionsExt}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf, StripPrefixError}; use std::str::FromStr; use std::string::ToString; @@ -49,6 +45,9 @@ use uucore::fs::{ }; use walkdir::WalkDir; +mod platform; +use platform::copy_on_write; + quick_error! { #[derive(Debug)] pub enum Error { @@ -224,16 +223,6 @@ pub struct Options { verbose: bool, } -// From /usr/include/linux/fs.h: -// #define FICLONE _IOW(0x94, 9, int) -#[cfg(any(target_os = "linux", target_os = "android"))] -// Use a macro as libc::ioctl expects u32 or u64 depending on the arch -macro_rules! FICLONE { - () => { - 0x40049409 - }; -} - static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; static LONG_HELP: &str = ""; static EXIT_ERR: i32 = 1; @@ -1589,26 +1578,7 @@ fn copy_helper( } else if source_is_symlink { copy_link(source, dest, symlinked_files)?; } else { - #[cfg(target_os = "macos")] - copy_on_write_macos( - source, - dest, - options.reflink_mode, - options.sparse_mode, - context, - )?; - - #[cfg(any(target_os = "linux", target_os = "android"))] - copy_on_write_linux( - source, - dest, - options.reflink_mode, - options.sparse_mode, - context, - )?; - - #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] - copy_no_cow_fallback( + copy_on_write( source, dest, options.reflink_mode, @@ -1664,180 +1634,6 @@ fn copy_link( symlink_file(&link, &dest, &context_for(&link, &dest), symlinked_files) } -/// Copies `source` to `dest` for systems without copy-on-write -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] -fn copy_no_cow_fallback( - source: &Path, - dest: &Path, - reflink_mode: ReflinkMode, - sparse_mode: SparseMode, - context: &str, -) -> CopyResult<()> { - if reflink_mode != ReflinkMode::Never { - return Err("--reflink is only supported on linux and macOS" - .to_string() - .into()); - } - if sparse_mode != SparseMode::Auto { - return Err("--sparse is only supported on linux".to_string().into()); - } - - fs::copy(source, dest).context(context)?; - - Ok(()) -} - -/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. -/// -/// If `fallback` is true and there is a failure performing the clone, -/// then this function performs a standard [`std::fs::copy`]. Otherwise, -/// this function returns an error. -#[cfg(any(target_os = "linux", target_os = "android"))] -fn clone

(source: P, dest: P, fallback: bool) -> std::io::Result<()> -where - P: AsRef, -{ - let src_file = File::open(&source)?; - let dst_file = File::create(&dest)?; - let src_fd = src_file.as_raw_fd(); - let dst_fd = dst_file.as_raw_fd(); - let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) }; - if result != 0 { - if fallback { - std::fs::copy(source, dest).map(|_| ()) - } else { - Err(std::io::Error::last_os_error()) - } - } else { - Ok(()) - } -} - -/// Perform a sparse copy from one file to another. -#[cfg(any(target_os = "linux", target_os = "android"))] -fn sparse_copy

(source: P, dest: P) -> std::io::Result<()> -where - P: AsRef, -{ - use std::os::unix::prelude::MetadataExt; - - let mut src_file = File::open(source)?; - let dst_file = File::create(dest)?; - let dst_fd = dst_file.as_raw_fd(); - - let size: usize = src_file.metadata()?.size().try_into().unwrap(); - if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 { - return Err(std::io::Error::last_os_error()); - } - - let blksize = dst_file.metadata()?.blksize(); - let mut buf: Vec = vec![0; blksize.try_into().unwrap()]; - let mut current_offset: usize = 0; - - // TODO Perhaps we can employ the "fiemap ioctl" API to get the - // file extent mappings: - // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html - while current_offset < size { - let this_read = src_file.read(&mut buf)?; - if buf.iter().any(|&x| x != 0) { - unsafe { - libc::pwrite( - dst_fd, - buf.as_ptr() as *const libc::c_void, - this_read, - current_offset.try_into().unwrap(), - ) - }; - } - current_offset += this_read; - } - Ok(()) -} - -/// Copies `source` to `dest` using copy-on-write if possible. -#[cfg(any(target_os = "linux", target_os = "android"))] -fn copy_on_write_linux( - source: &Path, - dest: &Path, - reflink_mode: ReflinkMode, - sparse_mode: SparseMode, - context: &str, -) -> CopyResult<()> { - let result = match (reflink_mode, sparse_mode) { - (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()), - (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest), - (ReflinkMode::Auto, _) => clone(source, dest, true), - (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false), - (ReflinkMode::Always, _) => { - return Err("`--reflink=always` can be used only with --sparse=auto".into()) - } - }; - result.context(context)?; - Ok(()) -} - -/// Copies `source` to `dest` using copy-on-write if possible. -#[cfg(target_os = "macos")] -fn copy_on_write_macos( - source: &Path, - dest: &Path, - reflink_mode: ReflinkMode, - sparse_mode: SparseMode, - context: &str, -) -> CopyResult<()> { - if sparse_mode != SparseMode::Auto { - return Err("--sparse is only supported on linux".to_string().into()); - } - - // Extract paths in a form suitable to be passed to a syscall. - // The unwrap() is safe because they come from the command-line and so contain non nul - // character. - let src = CString::new(source.as_os_str().as_bytes()).unwrap(); - let dst = CString::new(dest.as_os_str().as_bytes()).unwrap(); - - // clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it - // for backward compatibility. - let clonefile = CString::new("clonefile").unwrap(); - let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) }; - - let mut error = 0; - if !raw_pfn.is_null() { - // Call clonefile(2). - // Safety: Casting a C function pointer to a rust function value is one of the few - // blessed uses of `transmute()`. - unsafe { - let pfn: extern "C" fn( - src: *const libc::c_char, - dst: *const libc::c_char, - flags: u32, - ) -> libc::c_int = std::mem::transmute(raw_pfn); - error = pfn(src.as_ptr(), dst.as_ptr(), 0); - if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists { - // clonefile(2) fails if the destination exists. Remove it and try again. Do not - // bother to check if removal worked because we're going to try to clone again. - let _ = fs::remove_file(dest); - error = pfn(src.as_ptr(), dst.as_ptr(), 0); - } - } - } - - if raw_pfn.is_null() || error != 0 { - // clonefile(2) is either not supported or it errored out (possibly because the FS does not - // support COW). - match reflink_mode { - ReflinkMode::Always => { - return Err( - format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), - ) - } - ReflinkMode::Auto => fs::copy(source, dest).context(context)?, - ReflinkMode::Never => fs::copy(source, dest).context(context)?, - }; - } - - Ok(()) -} - /// Generate an error message if `target` is not the correct `target_type` pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { match (target_type, target.is_dir()) { diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs new file mode 100644 index 000000000..31aacdbca --- /dev/null +++ b/src/uu/cp/src/platform/linux.rs @@ -0,0 +1,110 @@ +// * 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. +// spell-checker:ignore ficlone reflink ftruncate pwrite +use std::fs::File; +use std::io::Read; +use std::os::unix::io::AsRawFd; +use std::path::Path; + +use quick_error::ResultExt; + +use crate::{CopyResult, ReflinkMode, SparseMode}; + +// From /usr/include/linux/fs.h: +// #define FICLONE _IOW(0x94, 9, int) +// Use a macro as libc::ioctl expects u32 or u64 depending on the arch +macro_rules! FICLONE { + () => { + 0x40049409 + }; +} + +/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. +/// +/// If `fallback` is true and there is a failure performing the clone, +/// then this function performs a standard [`std::fs::copy`]. Otherwise, +/// this function returns an error. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn clone

(source: P, dest: P, fallback: bool) -> std::io::Result<()> +where + P: AsRef, +{ + let src_file = File::open(&source)?; + let dst_file = File::create(&dest)?; + let src_fd = src_file.as_raw_fd(); + let dst_fd = dst_file.as_raw_fd(); + let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) }; + if result != 0 { + if fallback { + std::fs::copy(source, dest).map(|_| ()) + } else { + Err(std::io::Error::last_os_error()) + } + } else { + Ok(()) + } +} + +/// Perform a sparse copy from one file to another. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn sparse_copy

(source: P, dest: P) -> std::io::Result<()> +where + P: AsRef, +{ + use std::os::unix::prelude::MetadataExt; + + let mut src_file = File::open(source)?; + let dst_file = File::create(dest)?; + let dst_fd = dst_file.as_raw_fd(); + + let size: usize = src_file.metadata()?.size().try_into().unwrap(); + if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 { + return Err(std::io::Error::last_os_error()); + } + + let blksize = dst_file.metadata()?.blksize(); + let mut buf: Vec = vec![0; blksize.try_into().unwrap()]; + let mut current_offset: usize = 0; + + // TODO Perhaps we can employ the "fiemap ioctl" API to get the + // file extent mappings: + // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html + while current_offset < size { + let this_read = src_file.read(&mut buf)?; + if buf.iter().any(|&x| x != 0) { + unsafe { + libc::pwrite( + dst_fd, + buf.as_ptr() as *const libc::c_void, + this_read, + current_offset.try_into().unwrap(), + ) + }; + } + current_offset += this_read; + } + Ok(()) +} + +/// Copies `source` to `dest` using copy-on-write if possible. +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, +) -> CopyResult<()> { + let result = match (reflink_mode, sparse_mode) { + (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()), + (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest), + (ReflinkMode::Auto, _) => clone(source, dest, true), + (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false), + (ReflinkMode::Always, _) => { + return Err("`--reflink=always` can be used only with --sparse=auto".into()) + } + }; + result.context(context)?; + Ok(()) +} diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs new file mode 100644 index 000000000..7f1524154 --- /dev/null +++ b/src/uu/cp/src/platform/macos.rs @@ -0,0 +1,74 @@ +// * 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. +// spell-checker:ignore reflink +use std::ffi::CString; +use std::fs; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use quick_error::ResultExt; + +use crate::{CopyResult, ReflinkMode, SparseMode}; + +/// Copies `source` to `dest` using copy-on-write if possible. +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, +) -> CopyResult<()> { + if sparse_mode != SparseMode::Auto { + return Err("--sparse is only supported on linux".to_string().into()); + } + + // Extract paths in a form suitable to be passed to a syscall. + // The unwrap() is safe because they come from the command-line and so contain non nul + // character. + let src = CString::new(source.as_os_str().as_bytes()).unwrap(); + let dst = CString::new(dest.as_os_str().as_bytes()).unwrap(); + + // clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it + // for backward compatibility. + let clonefile = CString::new("clonefile").unwrap(); + let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) }; + + let mut error = 0; + if !raw_pfn.is_null() { + // Call clonefile(2). + // Safety: Casting a C function pointer to a rust function value is one of the few + // blessed uses of `transmute()`. + unsafe { + let pfn: extern "C" fn( + src: *const libc::c_char, + dst: *const libc::c_char, + flags: u32, + ) -> libc::c_int = std::mem::transmute(raw_pfn); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists { + // clonefile(2) fails if the destination exists. Remove it and try again. Do not + // bother to check if removal worked because we're going to try to clone again. + let _ = fs::remove_file(dest); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + } + } + } + + if raw_pfn.is_null() || error != 0 { + // clonefile(2) is either not supported or it errored out (possibly because the FS does not + // support COW). + match reflink_mode { + ReflinkMode::Always => { + return Err( + format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), + ) + } + ReflinkMode::Auto => fs::copy(source, dest).context(context)?, + ReflinkMode::Never => fs::copy(source, dest).context(context)?, + }; + } + + Ok(()) +} diff --git a/src/uu/cp/src/platform/mod.rs b/src/uu/cp/src/platform/mod.rs new file mode 100644 index 000000000..9dbcefa80 --- /dev/null +++ b/src/uu/cp/src/platform/mod.rs @@ -0,0 +1,18 @@ +// * 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. +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub(crate) use self::macos::copy_on_write; + +#[cfg(any(target_os = "linux", target_os = "android"))] +mod linux; +#[cfg(any(target_os = "linux", target_os = "android"))] +pub(crate) use self::linux::copy_on_write; + +#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +mod other; +#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +pub(crate) use self::other::copy_on_write; diff --git a/src/uu/cp/src/platform/other.rs b/src/uu/cp/src/platform/other.rs new file mode 100644 index 000000000..b70da2f23 --- /dev/null +++ b/src/uu/cp/src/platform/other.rs @@ -0,0 +1,33 @@ +// * 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. +// spell-checker:ignore reflink +use std::fs; +use std::path::Path; + +use quick_error::ResultExt; + +use crate::{CopyResult, ReflinkMode, SparseMode}; + +/// Copies `source` to `dest` for systems without copy-on-write +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, +) -> CopyResult<()> { + if reflink_mode != ReflinkMode::Never { + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + } + if sparse_mode != SparseMode::Auto { + return Err("--sparse is only supported on linux".to_string().into()); + } + + fs::copy(source, dest).context(context)?; + + Ok(()) +} From 32112d627a66cad5d7c148eb59d84164058a4cc5 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 6 Oct 2022 09:34:48 +0200 Subject: [PATCH 2/2] Add fiemap to the spelling --- src/uu/cp/src/platform/linux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 31aacdbca..7f47b884a 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -2,7 +2,7 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore ficlone reflink ftruncate pwrite +// spell-checker:ignore ficlone reflink ftruncate pwrite fiemap use std::fs::File; use std::io::Read; use std::os::unix::io::AsRawFd;