diff --git a/Cargo.lock b/Cargo.lock index e04687d263a..ef562bb21cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,9 +166,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" dependencies = [ "arrayref", "arrayvec", @@ -236,11 +236,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -673,12 +679,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.1" +version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ "nix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1303,12 +1309,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.4.2", "cfg-if", + "cfg_aliases", "libc", ] diff --git a/Cargo.toml b/Cargo.toml index 959982d86e5..6e9bf95babc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -263,7 +263,7 @@ binary-heap-plus = "0.5.0" bstr = "1.9" bytecount = "0.6.7" byteorder = "1.5.0" -chrono = { version = "^0.4.34", default-features = false, features = [ +chrono = { version = "^0.4.35", default-features = false, features = [ "std", "alloc", "clock", @@ -294,7 +294,7 @@ lscolors = { version = "0.16.0", default-features = false, features = [ ] } memchr = "2" memmap2 = "0.9" -nix = { version = "0.27", default-features = false } +nix = { version = "0.28", default-features = false } nom = "7.1.3" notify = { version = "=6.0.1", features = ["macos_kqueue"] } num-bigint = "0.4.4" @@ -340,7 +340,7 @@ sha1 = "0.10.6" sha2 = "0.10.8" sha3 = "0.10.8" blake2b_simd = "1.0.2" -blake3 = "1.5.0" +blake3 = "1.5.1" sm3 = "0.4.2" digest = "0.10.7" diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index 6c2b6d3dac3..13daae84d7f 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -5,7 +5,10 @@ use super::{CatResult, FdReadable, InputHandle}; use nix::unistd; -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::{ + fd::AsFd, + unix::io::{AsRawFd, RawFd}, +}; use uucore::pipes::{pipe, splice, splice_exact}; @@ -20,9 +23,9 @@ const BUF_SIZE: usize = 1024 * 16; /// The `bool` in the result value indicates if we need to fall back to normal /// copying or not. False means we don't have to. #[inline] -pub(super) fn write_fast_using_splice( +pub(super) fn write_fast_using_splice( handle: &InputHandle, - write_fd: &impl AsRawFd, + write_fd: &S, ) -> CatResult { let (pipe_rd, pipe_wr) = pipe()?; @@ -38,7 +41,7 @@ pub(super) fn write_fast_using_splice( // 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(), write_fd.as_raw_fd(), n)?; + copy_exact(pipe_rd.as_raw_fd(), write_fd, n)?; return Ok(true); } } @@ -52,7 +55,7 @@ pub(super) fn write_fast_using_splice( /// Move exactly `num_bytes` bytes from `read_fd` to `write_fd`. /// /// Panics if not enough bytes can be read. -fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { +fn copy_exact(read_fd: RawFd, write_fd: &impl AsFd, num_bytes: usize) -> nix::Result<()> { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; while left > 0 { diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index ec111c853fd..1a804bd3bbf 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -154,6 +154,7 @@ pub fn uu_app() -> Command { .override_usage(format_usage(USAGE)) .infer_long_args(true) .disable_help_flag(true) + .args_override_self(true) .arg( Arg::new(options::HELP) .long(options::HELP) @@ -163,7 +164,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::dereference::DEREFERENCE) .long(options::dereference::DEREFERENCE) - .conflicts_with(options::dereference::NO_DEREFERENCE) + .overrides_with(options::dereference::NO_DEREFERENCE) .help( "Affect the referent of each symbolic link (this is the default), \ rather than the symbolic link itself.", @@ -180,7 +181,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::preserve_root::PRESERVE_ROOT) .long(options::preserve_root::PRESERVE_ROOT) - .conflicts_with(options::preserve_root::NO_PRESERVE_ROOT) + .overrides_with(options::preserve_root::NO_PRESERVE_ROOT) .help("Fail to operate recursively on '/'.") .action(ArgAction::SetTrue), ) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 9a3a0848342..778ddf843b6 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet}; use std::env; #[cfg(not(windows))] use std::ffi::CString; -use std::fs::{self, File, OpenOptions}; +use std::fs::{self, File, Metadata, OpenOptions, Permissions}; use std::io; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; @@ -780,7 +780,11 @@ impl CopyMode { { Self::Update } else if matches.get_flag(options::ATTRIBUTES_ONLY) { - Self::AttrOnly + if matches.get_flag(options::REMOVE_DESTINATION) { + Self::Copy + } else { + Self::AttrOnly + } } else { Self::Copy } @@ -1635,153 +1639,59 @@ fn aligned_ancestors<'a>(source: &'a Path, dest: &'a Path) -> Vec<(&'a Path, &'a result } -/// Copy the a file from `source` to `dest`. `source` will be dereferenced if -/// `options.dereference` is set to true. `dest` will be dereferenced only if -/// the source was not a symlink. -/// -/// Behavior when copying to existing files is contingent on the -/// `options.overwrite` mode. If a file is skipped, the return type -/// should be `Error:Skipped` -/// -/// The original permissions of `source` will be copied to `dest` -/// after a successful copy. -#[allow(clippy::cognitive_complexity)] -fn copy_file( +fn print_verbose_output( + parents: bool, progress_bar: &Option, source: &Path, dest: &Path, - options: &Options, - symlinked_files: &mut HashSet, - copied_files: &mut HashMap, - source_in_command_line: bool, -) -> CopyResult<()> { - if (options.update == UpdateMode::ReplaceIfOlder || options.update == UpdateMode::ReplaceNone) - && options.overwrite == OverwriteMode::Interactive(ClobberMode::Standard) - { - // `cp -i --update old new` when `new` exists doesn't copy anything - // and exit with 0 - return Ok(()); - } - - // Fail if dest is a dangling symlink or a symlink this program created previously - if dest.is_symlink() { - if FileInformation::from_path(dest, false) - .map(|info| symlinked_files.contains(&info)) - .unwrap_or(false) - { - return Err(Error::Error(format!( - "will not copy '{}' through just-created symlink '{}'", - source.display(), - dest.display() - ))); - } - let copy_contents = options.dereference(source_in_command_line) || !source.is_symlink(); - if copy_contents - && !dest.exists() - && !matches!( - options.overwrite, - OverwriteMode::Clobber(ClobberMode::RemoveDestination) - ) - && !is_symlink_loop(dest) - && std::env::var_os("POSIXLY_CORRECT").is_none() - { - return Err(Error::Error(format!( - "not writing through dangling symlink '{}'", - dest.display() - ))); - } - if paths_refer_to_same_file(source, dest, true) - && matches!( - options.overwrite, - OverwriteMode::Clobber(ClobberMode::RemoveDestination) - ) - { - fs::remove_file(dest)?; - } - } - - if are_hardlinks_to_same_file(source, dest) - && matches!( - options.overwrite, - OverwriteMode::Clobber(ClobberMode::RemoveDestination) - ) - { - fs::remove_file(dest)?; - } - - if file_or_link_exists(dest) { - if are_hardlinks_to_same_file(source, dest) - && !options.force() - && options.backup == BackupMode::NoBackup - && source != dest - || (source == dest && options.copy_mode == CopyMode::Link) - { - return Ok(()); - } - handle_existing_dest(source, dest, options, source_in_command_line)?; - } - - if options.preserve_hard_links() { - // if we encounter a matching device/inode pair in the source tree - // we can arrange to create a hard link between the corresponding names - // in the destination tree. - if let Some(new_source) = copied_files.get( - &FileInformation::from_path(source, options.dereference(source_in_command_line)) - .context(format!("cannot stat {}", source.quote()))?, - ) { - std::fs::hard_link(new_source, dest)?; - return Ok(()); - }; +) { + if let Some(pb) = progress_bar { + // Suspend (hide) the progress bar so the println won't overlap with the progress bar. + pb.suspend(|| { + print_paths(parents, source, dest); + }); + } else { + print_paths(parents, source, dest); } +} - if options.verbose { - if let Some(pb) = progress_bar { - // Suspend (hide) the progress bar so the println won't overlap with the progress bar. - pb.suspend(|| { - if options.parents { - // For example, if copying file `a/b/c` and its parents - // to directory `d/`, then print - // - // a -> d/a - // a/b -> d/a/b - // - for (x, y) in aligned_ancestors(source, dest) { - println!("{} -> {}", x.display(), y.display()); - } - } - - println!("{}", context_for(source, dest)); - }); - } else { - if options.parents { - // For example, if copying file `a/b/c` and its parents - // to directory `d/`, then print - // - // a -> d/a - // a/b -> d/a/b - // - for (x, y) in aligned_ancestors(source, dest) { - println!("{} -> {}", x.display(), y.display()); - } - } - - println!("{}", context_for(source, dest)); +fn print_paths(parents: bool, source: &Path, dest: &Path) { + if parents { + // For example, if copying file `a/b/c` and its parents + // to directory `d/`, then print + // + // a -> d/a + // a/b -> d/a/b + // + for (x, y) in aligned_ancestors(source, dest) { + println!("{} -> {}", x.display(), y.display()); } } - // Calculate the context upfront before canonicalizing the path - let context = context_for(source, dest); - let context = context.as_str(); + println!("{}", context_for(source, dest)); +} - let source_metadata = { - let result = if options.dereference(source_in_command_line) { - fs::metadata(source) - } else { - fs::symlink_metadata(source) - }; - result.context(context)? - }; +/// Handles the copy mode for a file copy operation. +/// +/// This function determines how to copy a file based on the provided options. +/// It supports different copy modes, including hard linking, copying, symbolic linking, updating, and attribute-only copying. +/// It also handles file backups, overwriting, and dereferencing based on the provided options. +/// +/// # Returns +/// +/// * `Ok(())` - The file was copied successfully. +/// * `Err(CopyError)` - An error occurred while copying the file. +fn handle_copy_mode( + source: &Path, + dest: &Path, + options: &Options, + context: &str, + source_metadata: Metadata, + symlinked_files: &mut HashSet, + source_in_command_line: bool, +) -> CopyResult<()> { let source_file_type = source_metadata.file_type(); + let source_is_symlink = source_file_type.is_symlink(); #[cfg(unix)] @@ -1789,24 +1699,6 @@ fn copy_file( #[cfg(not(unix))] let source_is_fifo = false; - let dest_permissions = if dest.exists() { - dest.symlink_metadata().context(context)?.permissions() - } else { - #[allow(unused_mut)] - let mut permissions = source_metadata.permissions(); - #[cfg(unix)] - { - let mut mode = handle_no_preserve_mode(options, permissions.mode()); - - // apply umask - use uucore::mode::get_umask; - mode &= !get_umask(); - - permissions.set_mode(mode); - } - permissions - }; - match options.copy_mode { CopyMode::Link => { if dest.exists() { @@ -1909,6 +1801,196 @@ fn copy_file( } }; + Ok(()) +} + +/// Calculates the permissions for the destination file in a copy operation. +/// +/// If the destination file already exists, its current permissions are returned. +/// If the destination file does not exist, the source file's permissions are used, +/// with the `no-preserve` option and the umask taken into account on Unix platforms. +/// # Returns +/// +/// * `Ok(Permissions)` - The calculated permissions for the destination file. +/// * `Err(CopyError)` - An error occurred while getting the metadata of the destination file. +/// Allow unused variables for Windows (on options) +#[allow(unused_variables)] +fn calculate_dest_permissions( + dest: &Path, + source_metadata: &Metadata, + options: &Options, + context: &str, +) -> CopyResult { + if dest.exists() { + Ok(dest.symlink_metadata().context(context)?.permissions()) + } else { + #[cfg(unix)] + { + let mut permissions = source_metadata.permissions(); + let mode = handle_no_preserve_mode(options, permissions.mode()); + + // Apply umask + use uucore::mode::get_umask; + let mode = mode & !get_umask(); + permissions.set_mode(mode); + Ok(permissions) + } + #[cfg(not(unix))] + { + let permissions = source_metadata.permissions(); + Ok(permissions) + } + } +} + +/// Copy the a file from `source` to `dest`. `source` will be dereferenced if +/// `options.dereference` is set to true. `dest` will be dereferenced only if +/// the source was not a symlink. +/// +/// Behavior when copying to existing files is contingent on the +/// `options.overwrite` mode. If a file is skipped, the return type +/// should be `Error:Skipped` +/// +/// The original permissions of `source` will be copied to `dest` +/// after a successful copy. +#[allow(clippy::cognitive_complexity)] +fn copy_file( + progress_bar: &Option, + source: &Path, + dest: &Path, + options: &Options, + symlinked_files: &mut HashSet, + copied_files: &mut HashMap, + source_in_command_line: bool, +) -> CopyResult<()> { + if (options.update == UpdateMode::ReplaceIfOlder || options.update == UpdateMode::ReplaceNone) + && options.overwrite == OverwriteMode::Interactive(ClobberMode::Standard) + { + // `cp -i --update old new` when `new` exists doesn't copy anything + // and exit with 0 + return Ok(()); + } + + // Fail if dest is a dangling symlink or a symlink this program created previously + if dest.is_symlink() { + if FileInformation::from_path(dest, false) + .map(|info| symlinked_files.contains(&info)) + .unwrap_or(false) + { + return Err(Error::Error(format!( + "will not copy '{}' through just-created symlink '{}'", + source.display(), + dest.display() + ))); + } + let copy_contents = options.dereference(source_in_command_line) || !source.is_symlink(); + if copy_contents + && !dest.exists() + && !matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + ) + && !is_symlink_loop(dest) + && std::env::var_os("POSIXLY_CORRECT").is_none() + { + return Err(Error::Error(format!( + "not writing through dangling symlink '{}'", + dest.display() + ))); + } + if paths_refer_to_same_file(source, dest, true) + && matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + ) + { + fs::remove_file(dest)?; + } + } + + if are_hardlinks_to_same_file(source, dest) + && matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + ) + { + fs::remove_file(dest)?; + } + + if file_or_link_exists(dest) + && (!options.attributes_only + || matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + )) + { + if are_hardlinks_to_same_file(source, dest) + && !options.force() + && options.backup == BackupMode::NoBackup + && source != dest + || (source == dest && options.copy_mode == CopyMode::Link) + { + return Ok(()); + } + handle_existing_dest(source, dest, options, source_in_command_line)?; + } + + if options.attributes_only + && source.is_symlink() + && !matches!( + options.overwrite, + OverwriteMode::Clobber(ClobberMode::RemoveDestination) + ) + { + return Err(format!( + "cannot change attribute {}: Source file is a non regular file", + dest.quote() + ) + .into()); + } + + if options.preserve_hard_links() { + // if we encounter a matching device/inode pair in the source tree + // we can arrange to create a hard link between the corresponding names + // in the destination tree. + if let Some(new_source) = copied_files.get( + &FileInformation::from_path(source, options.dereference(source_in_command_line)) + .context(format!("cannot stat {}", source.quote()))?, + ) { + std::fs::hard_link(new_source, dest)?; + return Ok(()); + }; + } + + if options.verbose { + print_verbose_output(options.parents, progress_bar, source, dest); + } + + // Calculate the context upfront before canonicalizing the path + let context = context_for(source, dest); + let context = context.as_str(); + + let source_metadata = { + let result = if options.dereference(source_in_command_line) { + fs::metadata(source) + } else { + fs::symlink_metadata(source) + }; + result.context(context)? + }; + + let dest_permissions = calculate_dest_permissions(dest, &source_metadata, options, context)?; + + handle_copy_mode( + source, + dest, + options, + context, + source_metadata, + symlinked_files, + source_in_command_line, + )?; + // TODO: implement something similar to gnu's lchown if !dest.is_symlink() { // Here, to match GNU semantics, we quietly ignore an error diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 9b3e3911eeb..93d6c63a97d 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -275,7 +275,7 @@ impl Parser { fn parse_n(val: &str) -> Result { let n = parse_bytes_with_opt_multiplier(val)?; - Ok(if val.ends_with('B') { + Ok(if val.contains('B') { Num::Bytes(n) } else { Num::Blocks(n) @@ -631,8 +631,9 @@ fn conversion_mode( #[cfg(test)] mod tests { - use crate::parseargs::parse_bytes_with_opt_multiplier; - + use crate::parseargs::{parse_bytes_with_opt_multiplier, Parser}; + use crate::Num; + use std::matches; const BIG: &str = "9999999999999999999999999999999999999999999999999999999999999"; #[test] @@ -659,4 +660,13 @@ mod tests { 2 * 2 * (3 * 2) // (1 * 2) * (2 * 1) * (3 * 2) ); } + #[test] + fn test_parse_n() { + for arg in ["1x8x4", "1c", "123b", "123w"] { + assert!(matches!(Parser::parse_n(arg), Ok(Num::Blocks(_)))); + } + for arg in ["1Bx8x4", "2Bx8", "2Bx8B", "2x8B"] { + assert!(matches!(Parser::parse_n(arg), Ok(Num::Bytes(_)))); + } + } } diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 9955be7b292..331a50f6741 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -748,6 +748,18 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult> { /// Returns an empty Result or an error in case of failure. /// fn copy_file(from: &Path, to: &Path) -> UResult<()> { + // fs::copy fails if destination is a invalid symlink. + // so lets just remove all existing files at destination before copy. + if let Err(e) = fs::remove_file(to) { + if e.kind() != std::io::ErrorKind::NotFound { + show_error!( + "Failed to remove existing file {}. Error: {:?}", + to.display(), + e + ); + } + } + if from.as_os_str() == "/dev/null" { /* workaround a limitation of fs::copy * https://github.com/rust-lang/rust/issues/79390 diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 12810d847ad..333360f50ec 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2981,7 +2981,8 @@ fn display_date(metadata: &Metadata, config: &Config) -> String { Some(time) => { //Date is recent if from past 6 months //According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - let recent = time + chrono::TimeDelta::seconds(31_556_952 / 2) > chrono::Local::now(); + let recent = time + chrono::TimeDelta::try_seconds(31_556_952 / 2).unwrap() + > chrono::Local::now(); match &config.time_style { TimeStyle::FullIso => time.format("%Y-%m-%d %H:%M:%S.%f %z"), diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 9ee04826b48..40028c2fb5e 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -12,6 +12,7 @@ use rand::{Rng, RngCore}; use std::collections::HashSet; use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, Error, Read, Write}; +use std::ops::RangeInclusive; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::{format_usage, help_about, help_usage}; @@ -21,7 +22,7 @@ mod rand_read_adapter; enum Mode { Default(String), Echo(Vec), - InputRange((usize, usize)), + InputRange(RangeInclusive), } static USAGE: &str = help_usage!("shuf.md"); @@ -119,8 +120,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { find_seps(&mut evec, options.sep); shuf_exec(&mut evec, options)?; } - Mode::InputRange((b, e)) => { - shuf_exec(&mut (b, e), options)?; + Mode::InputRange(mut range) => { + shuf_exec(&mut range, options)?; } Mode::Default(filename) => { let fdata = read_input_file(&filename)?; @@ -289,14 +290,13 @@ impl<'a> Shufable for Vec<&'a [u8]> { } } -impl Shufable for (usize, usize) { +impl Shufable for RangeInclusive { type Item = usize; fn is_empty(&self) -> bool { - // Note: This is an inclusive range, so equality means there is 1 element. - self.0 > self.1 + self.is_empty() } fn choose(&self, rng: &mut WrappedRng) -> usize { - rng.gen_range(self.0..self.1) + rng.gen_range(self.clone()) } type PartialShuffleIterator<'b> = NonrepeatingIterator<'b> where Self: 'b; fn partial_shuffle<'b>( @@ -304,7 +304,7 @@ impl Shufable for (usize, usize) { rng: &'b mut WrappedRng, amount: usize, ) -> Self::PartialShuffleIterator<'b> { - NonrepeatingIterator::new(self.0, self.1, rng, amount) + NonrepeatingIterator::new(self.clone(), rng, amount) } } @@ -314,8 +314,7 @@ enum NumberSet { } struct NonrepeatingIterator<'a> { - begin: usize, - end: usize, // exclusive + range: RangeInclusive, rng: &'a mut WrappedRng, remaining_count: usize, buf: NumberSet, @@ -323,19 +322,19 @@ struct NonrepeatingIterator<'a> { impl<'a> NonrepeatingIterator<'a> { fn new( - begin: usize, - end: usize, + range: RangeInclusive, rng: &'a mut WrappedRng, amount: usize, ) -> NonrepeatingIterator { - let capped_amount = if begin > end { + let capped_amount = if range.start() > range.end() { 0 + } else if *range.start() == 0 && *range.end() == std::usize::MAX { + amount } else { - amount.min(end - begin) + amount.min(range.end() - range.start() + 1) }; NonrepeatingIterator { - begin, - end, + range, rng, remaining_count: capped_amount, buf: NumberSet::AlreadyListed(HashSet::default()), @@ -343,11 +342,11 @@ impl<'a> NonrepeatingIterator<'a> { } fn produce(&mut self) -> usize { - debug_assert!(self.begin <= self.end); + debug_assert!(self.range.start() <= self.range.end()); match &mut self.buf { NumberSet::AlreadyListed(already_listed) => { let chosen = loop { - let guess = self.rng.gen_range(self.begin..self.end); + let guess = self.rng.gen_range(self.range.clone()); let newly_inserted = already_listed.insert(guess); if newly_inserted { break guess; @@ -356,9 +355,11 @@ impl<'a> NonrepeatingIterator<'a> { // Once a significant fraction of the interval has already been enumerated, // the number of attempts to find a number that hasn't been chosen yet increases. // Therefore, we need to switch at some point from "set of already returned values" to "list of remaining values". - let range_size = self.end - self.begin; + let range_size = (self.range.end() - self.range.start()).saturating_add(1); if number_set_should_list_remaining(already_listed.len(), range_size) { - let mut remaining = (self.begin..self.end) + let mut remaining = self + .range + .clone() .filter(|n| !already_listed.contains(n)) .collect::>(); assert!(remaining.len() >= self.remaining_count); @@ -381,7 +382,7 @@ impl<'a> Iterator for NonrepeatingIterator<'a> { type Item = usize; fn next(&mut self) -> Option { - if self.begin > self.end || self.remaining_count == 0 { + if self.range.is_empty() || self.remaining_count == 0 { return None; } self.remaining_count -= 1; @@ -462,7 +463,7 @@ fn shuf_exec(input: &mut impl Shufable, opts: Options) -> UResult<()> { Ok(()) } -fn parse_range(input_range: &str) -> Result<(usize, usize), String> { +fn parse_range(input_range: &str) -> Result, String> { if let Some((from, to)) = input_range.split_once('-') { let begin = from .parse::() @@ -470,7 +471,11 @@ fn parse_range(input_range: &str) -> Result<(usize, usize), String> { let end = to .parse::() .map_err(|_| format!("invalid input range: {}", to.quote()))?; - Ok((begin, end + 1)) + if begin <= end || begin == end + 1 { + Ok(begin..=end) + } else { + Err(format!("invalid input range: {}", input_range.quote())) + } } else { Err(format!("invalid input range: {}", input_range.quote())) } diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index 2c8e154e8b1..eac57151be9 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -5,7 +5,7 @@ // spell-checker:ignore parenb parodd cmspar hupcl cstopb cread clocal crtscts CSIZE // spell-checker:ignore ignbrk brkint ignpar parmrk inpck istrip inlcr igncr icrnl ixoff ixon iuclc ixany imaxbel iutf -// spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofill ofdel nldly crdly tabdly bsdly vtdly ffdly +// spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofdel nldly crdly tabdly bsdly vtdly ffdly // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc // spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase // spell-checker:ignore sigquit sigtstp @@ -86,14 +86,6 @@ pub const OUTPUT_FLAGS: &[Flag] = &[ target_os = "linux", target_os = "macos" ))] - Flag::new("ofill", O::OFILL), - #[cfg(any( - target_os = "android", - target_os = "haiku", - target_os = "ios", - target_os = "linux", - target_os = "macos" - ))] Flag::new("ofdel", O::OFDEL), #[cfg(any( target_os = "android", diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index ebdff8d2116..fe1783b214a 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -326,12 +326,12 @@ fn update_times( // If `follow` is `true`, the function will try to follow symlinks // If `follow` is `false` or the symlink is broken, the function will return metadata of the symlink itself fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> { - let metadata = match fs::metadata(path) { - Ok(metadata) => metadata, - Err(e) if e.kind() == std::io::ErrorKind::NotFound && !follow => fs::symlink_metadata(path) - .map_err_context(|| format!("failed to get attributes of {}", path.quote()))?, - Err(e) => return Err(e.into()), - }; + let metadata = if follow { + fs::metadata(path).or_else(|_| fs::symlink_metadata(path)) + } else { + fs::symlink_metadata(path) + } + .map_err_context(|| format!("failed to get attributes of {}", path.quote()))?; Ok(( FileTime::from_last_access_time(&metadata), @@ -433,7 +433,7 @@ fn parse_timestamp(s: &str) -> UResult { // only care about the timestamp anyway. // Tested in gnu/tests/touch/60-seconds if local.second() == 59 && ts.ends_with(".60") { - local += Duration::seconds(1); + local += Duration::try_seconds(1).unwrap(); } // Due to daylight saving time switch, local time can jump from 1:59 AM to @@ -441,7 +441,7 @@ fn parse_timestamp(s: &str) -> UResult { // valid. If we are within this jump, chrono takes the offset from before // the jump. If we then jump forward an hour, we get the new corrected // offset. Jumping back will then now correctly take the jump into account. - let local2 = local + Duration::hours(1) - Duration::hours(1); + let local2 = local + Duration::try_hours(1).unwrap() - Duration::try_hours(1).unwrap(); if local.hour() != local2.hour() { return Err(USimpleError::new( 1, diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 5565de6a16d..cfc9b11cb91 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -339,6 +339,32 @@ impl Sequence { pub trait SymbolTranslator { fn translate(&mut self, current: u8) -> Option; + + /// Takes two SymbolTranslators and creates a new SymbolTranslator over both in sequence. + /// + /// This behaves pretty much identical to [`Iterator::chain`]. + fn chain(self, other: T) -> ChainedSymbolTranslator + where + Self: Sized, + { + ChainedSymbolTranslator:: { + stage_a: self, + stage_b: other, + } + } +} + +pub struct ChainedSymbolTranslator { + stage_a: A, + stage_b: B, +} + +impl SymbolTranslator for ChainedSymbolTranslator { + fn translate(&mut self, current: u8) -> Option { + self.stage_a + .translate(current) + .and_then(|c| self.stage_b.translate(c)) + } } #[derive(Debug)] diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs index 968682a264b..6f78f13db94 100644 --- a/src/uu/tr/src/tr.rs +++ b/src/uu/tr/src/tr.rs @@ -9,9 +9,10 @@ mod operation; mod unicode_table; use clap::{crate_version, Arg, ArgAction, Command}; -use nom::AsBytes; -use operation::{translate_input, Sequence, SqueezeOperation, TranslateOperation}; -use std::io::{stdin, stdout, BufReader, BufWriter}; +use operation::{ + translate_input, Sequence, SqueezeOperation, SymbolTranslator, TranslateOperation, +}; +use std::io::{stdin, stdout, BufWriter}; use uucore::{format_usage, help_about, help_section, help_usage, show}; use crate::operation::DeleteOperation; @@ -117,19 +118,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { truncate_set1_flag, )?; + // '*_op' are the operations that need to be applied, in order. if delete_flag { if squeeze_flag { - let mut delete_buffer = vec![]; - { - let mut delete_writer = BufWriter::new(&mut delete_buffer); - let delete_op = DeleteOperation::new(set1, complement_flag); - translate_input(&mut locked_stdin, &mut delete_writer, delete_op); - } - { - let mut squeeze_reader = BufReader::new(delete_buffer.as_bytes()); - let op = SqueezeOperation::new(set2, false); - translate_input(&mut squeeze_reader, &mut buffered_stdout, op); - } + let delete_op = DeleteOperation::new(set1, complement_flag); + let squeeze_op = SqueezeOperation::new(set2, false); + let op = delete_op.chain(squeeze_op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op); } else { let op = DeleteOperation::new(set1, complement_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op); @@ -139,17 +134,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let op = SqueezeOperation::new(set1, complement_flag); translate_input(&mut locked_stdin, &mut buffered_stdout, op); } else { - let mut translate_buffer = vec![]; - { - let mut writer = BufWriter::new(&mut translate_buffer); - let op = TranslateOperation::new(set1, set2.clone(), complement_flag)?; - translate_input(&mut locked_stdin, &mut writer, op); - } - { - let mut reader = BufReader::new(translate_buffer.as_bytes()); - let squeeze_op = SqueezeOperation::new(set2, false); - translate_input(&mut reader, &mut buffered_stdout, squeeze_op); - } + let translate_op = TranslateOperation::new(set1, set2.clone(), complement_flag)?; + let squeeze_op = SqueezeOperation::new(set2, false); + let op = translate_op.chain(squeeze_op); + translate_input(&mut locked_stdin, &mut buffered_stdout, op); } } else { let op = TranslateOperation::new(set1, set2, complement_flag)?; diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index efda4a7becc..b7d3aedcd22 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -9,7 +9,6 @@ use clap::{crate_version, Arg, ArgAction, Command}; use std::io::{IsTerminal, Write}; -use std::os::unix::io::AsRawFd; use uucore::error::{set_exit_code, UResult}; use uucore::{format_usage, help_about, help_usage}; @@ -37,8 +36,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = std::io::stdout(); - // Get the ttyname via nix - let name = nix::unistd::ttyname(std::io::stdin().as_raw_fd()); + let name = nix::unistd::ttyname(std::io::stdin()); let write_result = match name { Ok(name) => writeln!(stdout, "{}", name.display()), diff --git a/src/uucore/src/lib/features/pipes.rs b/src/uucore/src/lib/features/pipes.rs index 75749f72148..17c5b1b3245 100644 --- a/src/uucore/src/lib/features/pipes.rs +++ b/src/uucore/src/lib/features/pipes.rs @@ -8,7 +8,6 @@ use std::fs::File; use std::io::IoSlice; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::io::AsRawFd; -use std::os::unix::io::FromRawFd; #[cfg(any(target_os = "linux", target_os = "android"))] use nix::fcntl::SpliceFFlags; @@ -21,8 +20,7 @@ pub use nix::{Error, Result}; /// from the first. pub fn pipe() -> Result<(File, File)> { let (read, write) = nix::unistd::pipe()?; - // SAFETY: The file descriptors do not have other owners. - unsafe { Ok((File::from_raw_fd(read), File::from_raw_fd(write))) } + Ok((File::from(read), File::from(write))) } /// Less noisy wrapper around [`nix::fcntl::splice`]. diff --git a/src/uucore/src/lib/parser/parse_glob.rs b/src/uucore/src/lib/parser/parse_glob.rs index 9215dd7bf7d..a4e12f467a0 100644 --- a/src/uucore/src/lib/parser/parse_glob.rs +++ b/src/uucore/src/lib/parser/parse_glob.rs @@ -18,7 +18,11 @@ fn fix_negation(glob: &str) -> String { while i + 3 < chars.len() { if chars[i] == '[' && chars[i + 1] == '^' { match chars[i + 3..].iter().position(|x| *x == ']') { - None => (), + None => { + // if closing square bracket not found, stop looking for it + // again + break; + } Some(j) => { chars[i + 1] = '!'; i += j + 4; @@ -90,6 +94,11 @@ mod tests { assert_eq!(fix_negation("[[]] [^a]"), "[[]] [!a]"); assert_eq!(fix_negation("[[] [^a]"), "[[] [!a]"); assert_eq!(fix_negation("[]] [^a]"), "[]] [!a]"); + + // test that we don't look for closing square brackets unnecessarily + // Verifies issue #5584 + let chars = std::iter::repeat("^[").take(174571).collect::(); + assert_eq!(fix_negation(chars.as_str()), chars); } #[test] diff --git a/tests/by-util/test_chcon.rs b/tests/by-util/test_chcon.rs index 71405e451d0..a8dae9aed06 100644 --- a/tests/by-util/test_chcon.rs +++ b/tests/by-util/test_chcon.rs @@ -88,6 +88,33 @@ fn valid_context_on_valid_symlink() { assert_eq!(get_file_context(dir.plus("a.tmp")).unwrap(), a_context); } +#[test] +fn valid_context_on_valid_symlink_override_dereference() { + let (dir, mut cmd) = at_and_ucmd!(); + dir.touch("a.tmp"); + dir.symlink_file("a.tmp", "la.tmp"); + + let a_context = get_file_context(dir.plus("a.tmp")).unwrap(); + let la_context = get_file_context(dir.plus("la.tmp")).unwrap(); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + assert_ne!(a_context.as_deref(), Some(new_a_context)); + assert_ne!(la_context.as_deref(), Some(new_a_context)); + + cmd.args(&[ + "--verbose", + "--no-dereference", + "--dereference", + new_a_context, + ]) + .arg(dir.plus("la.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("a.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); + assert_eq!(get_file_context(dir.plus("la.tmp")).unwrap(), la_context); +} + #[test] fn valid_context_on_broken_symlink() { let (dir, mut cmd) = at_and_ucmd!(); @@ -104,6 +131,29 @@ fn valid_context_on_broken_symlink() { ); } +#[test] +fn valid_context_on_broken_symlink_after_deref() { + let (dir, mut cmd) = at_and_ucmd!(); + dir.symlink_file("a.tmp", "la.tmp"); + + let la_context = get_file_context(dir.plus("la.tmp")).unwrap(); + let new_la_context = "guest_u:object_r:etc_t:s0:c42"; + assert_ne!(la_context.as_deref(), Some(new_la_context)); + + cmd.args(&[ + "--verbose", + "--dereference", + "--no-dereference", + new_la_context, + ]) + .arg(dir.plus("la.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("la.tmp")).unwrap().as_deref(), + Some(new_la_context) + ); +} + #[test] fn valid_context_with_prior_xattributes() { let (dir, mut cmd) = at_and_ucmd!(); @@ -324,6 +374,30 @@ fn user_change() { ); } +#[test] +fn user_change_repeated() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let a_context = get_file_context(dir.plus("a.tmp")).unwrap(); + let new_a_context = if let Some(a_context) = a_context { + let mut components: Vec<_> = a_context.split(':').collect(); + components[0] = "guest_u"; + components.join(":") + } else { + set_file_context(dir.plus("a.tmp"), "unconfined_u:object_r:user_tmp_t:s0").unwrap(); + String::from("guest_u:object_r:user_tmp_t:s0") + }; + + cmd.args(&["--verbose", "--user=wrong", "--user=guest_u"]) + .arg(dir.plus("a.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("a.tmp")).unwrap(), + Some(new_a_context) + ); +} + #[test] fn role_change() { let (dir, mut cmd) = at_and_ucmd!(); @@ -421,6 +495,99 @@ fn valid_reference() { ); } +#[test] +fn valid_reference_repeat_flags() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + set_file_context(dir.plus("a.tmp"), new_a_context).unwrap(); + + dir.touch("b.tmp"); + let b_context = get_file_context(dir.plus("b.tmp")).unwrap(); + assert_ne!(b_context.as_deref(), Some(new_a_context)); + + cmd.arg("--verbose") + .arg("-vvRRHHLLPP") // spell-checker:disable-line + .arg("--no-preserve-root") + .arg("--no-preserve-root") + .arg("--preserve-root") + .arg("--preserve-root") + .arg("--dereference") + .arg("--dereference") + .arg("--no-dereference") + .arg("--no-dereference") + .arg(format!("--reference={}", dir.plus_as_string("a.tmp"))) + .arg(dir.plus("b.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("b.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); +} + +#[test] +fn valid_reference_repeated_reference() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + set_file_context(dir.plus("a.tmp"), new_a_context).unwrap(); + + dir.touch("wrong.tmp"); + let new_wrong_context = "guest_u:object_r:etc_t:s42:c0"; + set_file_context(dir.plus("wrong.tmp"), new_wrong_context).unwrap(); + + dir.touch("b.tmp"); + let b_context = get_file_context(dir.plus("b.tmp")).unwrap(); + assert_ne!(b_context.as_deref(), Some(new_a_context)); + + cmd.arg("--verbose") + .arg(format!("--reference={}", dir.plus_as_string("wrong.tmp"))) + .arg(format!("--reference={}", dir.plus_as_string("a.tmp"))) + .arg(dir.plus("b.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("b.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); + assert_eq!( + get_file_context(dir.plus("wrong.tmp")).unwrap().as_deref(), + Some(new_wrong_context) + ); +} + +#[test] +fn valid_reference_multi() { + let (dir, mut cmd) = at_and_ucmd!(); + + dir.touch("a.tmp"); + let new_a_context = "guest_u:object_r:etc_t:s0:c42"; + set_file_context(dir.plus("a.tmp"), new_a_context).unwrap(); + + dir.touch("b1.tmp"); + let b1_context = get_file_context(dir.plus("b1.tmp")).unwrap(); + assert_ne!(b1_context.as_deref(), Some(new_a_context)); + + dir.touch("b2.tmp"); + let b2_context = get_file_context(dir.plus("b2.tmp")).unwrap(); + assert_ne!(b2_context.as_deref(), Some(new_a_context)); + + cmd.arg("--verbose") + .arg(format!("--reference={}", dir.plus_as_string("a.tmp"))) + .arg(dir.plus("b1.tmp")) + .arg(dir.plus("b2.tmp")) + .succeeds(); + assert_eq!( + get_file_context(dir.plus("b1.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); + assert_eq!( + get_file_context(dir.plus("b2.tmp")).unwrap().as_deref(), + Some(new_a_context) + ); +} + fn get_file_context(path: impl AsRef) -> Result, selinux::errors::Error> { let path = path.as_ref(); match selinux::SecurityContext::of_path(path, false, false) { diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index be364d1f623..eca5ba0edff 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -124,10 +124,8 @@ fn test_preserve_root_symlink() { ] { let (at, mut ucmd) = at_and_ucmd!(); at.symlink_file(d, file); - let expected_error = format!( - "chgrp: it is dangerous to operate recursively on 'test_chgrp_symlink2root' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n", - //d, - ); + let expected_error = + "chgrp: it is dangerous to operate recursively on 'test_chgrp_symlink2root' (same as '/')\nchgrp: use --no-preserve-root to override this failsafe\n"; ucmd.arg("--preserve-root") .arg("-HR") .arg("bin") diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index d3cee58adc9..fc955db4c92 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -3781,7 +3781,7 @@ fn test_acl_preserve() { // calling the command directly. xattr requires some dev packages to be installed // and it adds a complex dependency just for a test match Command::new("setfacl") - .args(["-m", "group::rwx", &path1]) + .args(["-m", "group::rwx", path1]) .status() .map(|status| status.code()) { @@ -3800,3 +3800,58 @@ fn test_acl_preserve() { assert!(compare_xattrs(&file, &file_target)); } + +#[test] +fn test_cp_force_remove_destination_attributes_only_with_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("file1", "1"); + at.write("file2", "2"); + at.symlink_file("file1", "sym1"); + + scene + .ucmd() + .args(&[ + "-a", + "--remove-destination", + "--attributes-only", + "sym1", + "file2", + ]) + .succeeds(); + + assert!( + at.symlink_exists("file2"), + "file2 is not a symbolic link as expected" + ); + + assert_eq!( + at.read("file1"), + at.read("file2"), + "Contents of file1 and file2 do not match" + ); +} + +#[test] +fn test_cp_no_dereference_attributes_only_with_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file1", "1"); + at.write("file2", "2"); + at.write("file2.exp", "2"); + at.symlink_file("file1", "sym1"); + + let result = scene + .ucmd() + .args(&["--no-dereference", "--attributes-only", "sym1", "file2"]) + .fails(); + + assert_eq!(result.code(), 1, "cp command did not fail"); + + assert_eq!( + at.read("file2"), + at.read("file2.exp"), + "file2 content does not match expected" + ); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index a65f02fa4c7..def3fa8af00 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -294,6 +294,7 @@ fn test_date_for_no_permission_file() { use std::os::unix::fs::PermissionsExt; let file = std::fs::OpenOptions::new() .create(true) + .truncate(true) .write(true) .open(at.plus(FILE)) .unwrap(); diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 24aa6bfdf23..401a5c5ef70 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1405,6 +1405,36 @@ fn test_bytes_suffix() { .stdout_only("\0\0\0abcdef"); } +#[test] +// the recursive nature of the suffix allows any string with a 'B' in it treated as bytes. +fn test_bytes_suffix_recursive() { + new_ucmd!() + .args(&["count=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("abcd"); + new_ucmd!() + .args(&["skip=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("ef"); + new_ucmd!() + .args(&["iseek=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("ef"); + new_ucmd!() + .args(&["seek=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("\0\0\0\0abcdef"); + new_ucmd!() + .args(&["oseek=2Bx2", "status=none"]) + .pipe_in("abcdef") + .succeeds() + .stdout_only("\0\0\0\0abcdef"); +} + /// Test for "conv=sync" with a slow reader. #[cfg(not(windows))] #[test] diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 6b1d76e5527..5790c685fc2 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -8,7 +8,7 @@ use crate::common::util::{is_ci, run_ucmd_as_root, TestScenario}; use filetime::FileTime; use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(not(windows))] use std::process::Command; #[cfg(any(target_os = "linux", target_os = "android"))] use std::thread::sleep; @@ -610,13 +610,17 @@ fn test_install_copy_then_compare_file_with_extra_mode() { } const STRIP_TARGET_FILE: &str = "helloworld_installed"; -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(all(not(windows), not(target_os = "freebsd")))] const SYMBOL_DUMP_PROGRAM: &str = "objdump"; -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(target_os = "freebsd")] +const SYMBOL_DUMP_PROGRAM: &str = "llvm-objdump"; +#[cfg(not(windows))] const STRIP_SOURCE_FILE_SYMBOL: &str = "main"; fn strip_source_file() -> &'static str { - if cfg!(target_os = "macos") { + if cfg!(target_os = "freebsd") { + "helloworld_freebsd" + } else if cfg!(target_os = "macos") { "helloworld_macos" } else if cfg!(target_arch = "arm") || cfg!(target_arch = "aarch64") { "helloworld_android" @@ -626,8 +630,7 @@ fn strip_source_file() -> &'static str { } #[test] -// FixME: Freebsd fails on 'No such file or directory' -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(not(windows))] fn test_install_and_strip() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -650,8 +653,7 @@ fn test_install_and_strip() { } #[test] -// FixME: Freebsd fails on 'No such file or directory' -#[cfg(not(any(windows, target_os = "freebsd")))] +#[cfg(not(windows))] fn test_install_and_strip_with_program() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -677,8 +679,6 @@ fn test_install_and_strip_with_program() { #[cfg(all(unix, feature = "chmod"))] #[test] -// FixME: Freebsd fails on 'No such file or directory' -#[cfg(not(target_os = "freebsd"))] fn test_install_and_strip_with_program_hyphen() { let scene = TestScenario::new(util_name!()); @@ -715,6 +715,64 @@ fn test_install_and_strip_with_program_hyphen() { .stdout_is("./-dest\n"); } +#[cfg(all(unix, feature = "chmod"))] +#[test] +fn test_install_on_invalid_link_at_destination() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + at.mkdir("src"); + at.mkdir("dest"); + let src_dir = at.plus("src"); + let dst_dir = at.plus("dest"); + + at.touch("test.sh"); + at.symlink_file( + "/opt/FakeDestination", + &dst_dir.join("test.sh").to_string_lossy(), + ); + scene.ccmd("chmod").arg("+x").arg("test.sh").succeeds(); + at.symlink_file("test.sh", &src_dir.join("test.sh").to_string_lossy()); + + scene + .ucmd() + .current_dir(&src_dir) + .arg(src_dir.join("test.sh")) + .arg(dst_dir.join("test.sh")) + .succeeds() + .no_stderr() + .no_stdout(); +} + +#[cfg(all(unix, feature = "chmod"))] +#[test] +fn test_install_on_invalid_link_at_destination_and_dev_null_at_source() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + at.mkdir("src"); + at.mkdir("dest"); + let src_dir = at.plus("src"); + let dst_dir = at.plus("dest"); + + at.touch("test.sh"); + at.symlink_file( + "/opt/FakeDestination", + &dst_dir.join("test.sh").to_string_lossy(), + ); + scene.ccmd("chmod").arg("+x").arg("test.sh").succeeds(); + at.symlink_file("test.sh", &src_dir.join("test.sh").to_string_lossy()); + + scene + .ucmd() + .current_dir(&src_dir) + .arg("/dev/null") + .arg(dst_dir.join("test.sh")) + .succeeds() + .no_stderr() + .no_stdout(); +} + #[test] #[cfg(not(windows))] fn test_install_and_strip_with_invalid_program() { diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index dd05ffbcd0a..a53d7277bd9 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1590,7 +1590,7 @@ fn test_acl() { // calling the command directly. xattr requires some dev packages to be installed // and it adds a complex dependency just for a test match Command::new("setfacl") - .args(["-m", "group::rwx", &path1]) + .args(["-m", "group::rwx", path1]) .status() .map(|status| status.code()) { diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index af3c41f1bd1..823f0718f4b 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -26,13 +26,12 @@ fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String { } fn all_minutes(from: DateTime, to: DateTime) -> Vec { - let to = to + Duration::minutes(1); - // const FORMAT: &str = "%b %d %H:%M %Y"; + let to = to + Duration::try_minutes(1).unwrap(); let mut vec = vec![]; let mut current = from; while current < to { vec.push(current.format(DATE_TIME_FORMAT).to_string()); - current += Duration::minutes(1); + current += Duration::try_minutes(1).unwrap(); } vec } diff --git a/tests/by-util/test_shuf.rs b/tests/by-util/test_shuf.rs index 7b0af7c944c..8a991e43509 100644 --- a/tests/by-util/test_shuf.rs +++ b/tests/by-util/test_shuf.rs @@ -138,6 +138,99 @@ fn test_very_large_range_offset() { ); } +#[test] +fn test_range_repeat_no_overflow_1_max() { + let upper_bound = std::usize::MAX; + let result = new_ucmd!() + .arg("-rn1") + .arg(&format!("-i1-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_repeat_no_overflow_0_max_minus_1() { + let upper_bound = std::usize::MAX - 1; + let result = new_ucmd!() + .arg("-rn1") + .arg(&format!("-i0-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_permute_no_overflow_1_max() { + let upper_bound = std::usize::MAX; + let result = new_ucmd!() + .arg("-n1") + .arg(&format!("-i1-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_permute_no_overflow_0_max_minus_1() { + let upper_bound = std::usize::MAX - 1; + let result = new_ucmd!() + .arg("-n1") + .arg(&format!("-i0-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + +#[test] +fn test_range_permute_no_overflow_0_max() { + // NOTE: This is different from GNU shuf! + // GNU shuf accepts -i0-MAX-1 and -i1-MAX, but not -i0-MAX. + // This feels like a bug in GNU shuf. + let upper_bound = std::usize::MAX; + let result = new_ucmd!() + .arg("-n1") + .arg(&format!("-i0-{upper_bound}")) + .succeeds(); + result.no_stderr(); + + let result_seq: Vec = result + .stdout_str() + .split('\n') + .filter(|x| !x.is_empty()) + .map(|x| x.parse().unwrap()) + .collect(); + assert_eq!(result_seq.len(), 1, "Miscounted output length!"); +} + #[test] fn test_very_high_range_full() { let input_seq = vec![ @@ -626,7 +719,6 @@ fn test_shuf_multiple_input_line_count() { } #[test] -#[ignore = "known issue"] fn test_shuf_repeat_empty_range() { new_ucmd!() .arg("-ri4-3") @@ -653,3 +745,56 @@ fn test_shuf_repeat_empty_input() { .no_stdout() .stderr_only("shuf: no lines to repeat\n"); } + +#[test] +fn test_range_one_elem() { + new_ucmd!() + .arg("-i5-5") + .succeeds() + .no_stderr() + .stdout_only("5\n"); +} + +#[test] +fn test_range_empty() { + new_ucmd!().arg("-i5-4").succeeds().no_output(); +} + +#[test] +fn test_range_empty_minus_one() { + new_ucmd!() + .arg("-i5-3") + .fails() + .no_stdout() + .stderr_only("shuf: invalid input range: '5-3'\n"); +} + +#[test] +fn test_range_repeat_one_elem() { + new_ucmd!() + .arg("-n1") + .arg("-ri5-5") + .succeeds() + .no_stderr() + .stdout_only("5\n"); +} + +#[test] +fn test_range_repeat_empty() { + new_ucmd!() + .arg("-n1") + .arg("-ri5-4") + .fails() + .no_stdout() + .stderr_only("shuf: no lines to repeat\n"); +} + +#[test] +fn test_range_repeat_empty_minus_one() { + new_ucmd!() + .arg("-n1") + .arg("-ri5-3") + .fails() + .no_stdout() + .stderr_only("shuf: invalid input range: '5-3'\n"); +} diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 3151ca720ad..3af129d49d7 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -5,7 +5,7 @@ // spell-checker:ignore (formats) cymdhm cymdhms mdhm mdhms ymdhm ymdhms datetime mktime use crate::common::util::{AtPath, TestScenario}; -use filetime::FileTime; +use filetime::{self, set_symlink_file_times, FileTime}; use std::fs::remove_file; use std::path::PathBuf; @@ -32,7 +32,7 @@ fn set_file_times(at: &AtPath, path: &str, atime: FileTime, mtime: FileTime) { fn str_to_filetime(format: &str, s: &str) -> FileTime { let tm = chrono::NaiveDateTime::parse_from_str(s, format).unwrap(); - FileTime::from_unix_time(tm.timestamp(), tm.timestamp_subsec_nanos()) + FileTime::from_unix_time(tm.and_utc().timestamp(), tm.timestamp_subsec_nanos()) } #[test] @@ -854,3 +854,40 @@ fn test_touch_invalid_date_format() { .fails() .stderr_contains("touch: invalid date format '+1000000000000 years'"); } + +#[test] +#[cfg(not(target_os = "freebsd"))] +fn test_touch_symlink_with_no_deref() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "foo.txt"; + let symlink = "bar.txt"; + let time = FileTime::from_unix_time(123, 0); + + at.touch(target); + at.relative_symlink_file(target, symlink); + set_symlink_file_times(at.plus(symlink), time, time).unwrap(); + + ucmd.args(&["-a", "--no-dereference", symlink]).succeeds(); + // Modification time shouldn't be set to the destination's modification time + assert_eq!(time, get_symlink_times(&at, symlink).1); +} + +#[test] +#[cfg(not(target_os = "freebsd"))] +fn test_touch_reference_symlink_with_no_deref() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "foo.txt"; + let symlink = "bar.txt"; + let arg = "baz.txt"; + let time = FileTime::from_unix_time(123, 0); + + at.touch(target); + at.relative_symlink_file(target, symlink); + set_symlink_file_times(at.plus(symlink), time, time).unwrap(); + at.touch(arg); + + ucmd.args(&["--reference", symlink, "--no-dereference", arg]) + .succeeds(); + // Times should be taken from the symlink, not the destination + assert_eq!((time, time), get_symlink_times(&at, arg)); +} diff --git a/tests/fixtures/install/helloworld_freebsd b/tests/fixtures/install/helloworld_freebsd new file mode 100755 index 00000000000..bfd78863030 Binary files /dev/null and b/tests/fixtures/install/helloworld_freebsd differ