diff --git a/Cargo.lock b/Cargo.lock index 9827a789349..4c4815bee16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1156,9 +1156,9 @@ dependencies = [ [[package]] name = "humantime_to_duration" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46276b15cd421d8262968eb5c3a075ae4a40bd787762ca0ddfead00fd5f6fbce" +checksum = "bcf364c7a07c8beae52fdab733304ceceeb9213f4137d938a17bae365a0136f9" dependencies = [ "regex", "time", @@ -1318,9 +1318,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.143" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc207893e85c5d6be840e969b496b53d94cec8be2d501b214f50daa97fa8024" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libloading" @@ -1404,9 +1404,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.8" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +checksum = "7f9ff02d2efdc645fca1ee55f45545b996e7da776b5b60c4e170334457551693" dependencies = [ "libc", ] @@ -2552,6 +2552,7 @@ dependencies = [ "clap", "gcd", "libc", + "nix", "signal-hook", "uucore", ] diff --git a/Cargo.toml b/Cargo.toml index 0623f9e6af1..2434870965a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -286,11 +286,11 @@ fundu = "0.5.1" gcd = "2.3" glob = "0.3.1" half = "2.2" -humantime_to_duration = "0.1.3" +humantime_to_duration = "0.2.0" indicatif = "0.17" is-terminal = "0.4.6" itertools = "0.10.5" -libc = "0.2.143" +libc = "0.2.144" lscolors = { version = "0.14.0", default-features=false, features = ["nu-ansi-term"] } memchr = "2" nix = { version="0.26", default-features=false } diff --git a/docs/src/extensions.md b/docs/src/extensions.md index 7aaf9db14fb..281d8ef2e7b 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -5,6 +5,21 @@ features that are not supported by GNU coreutils. We take care not to introduce features that are incompatible with the GNU coreutils. Below is a list of uutils extensions. +## General + +GNU coreutils provides two ways to define short options taking an argument: + +``` +$ ls -w 80 +$ ls -w80 +``` + +We support a third way: + +``` +$ ls -w=80 +``` + ## `env` `env` has an additional `-f`/`--file` flag that can parse `.env` files and set diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 02dafc98ae4..b7a2ce58168 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -216,6 +216,10 @@ impl<'a, 'b> MDWriter<'a, 'b> { } else if let Some(f) = get_zip_content(zip, &format!("pages/linux/{}.md", self.name)) { f } else { + println!( + "Warning: Could not find tldr examples for page '{}'", + self.name + ); return Ok(()); }; diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index afc25c7367e..3ec8cede01a 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -24,15 +24,33 @@ use uucore::{help_about, help_usage}; const ABOUT: &str = help_about!("basenc.md"); const USAGE: &str = help_usage!("basenc.md"); -const ENCODINGS: &[(&str, Format)] = &[ - ("base64", Format::Base64), - ("base64url", Format::Base64Url), - ("base32", Format::Base32), - ("base32hex", Format::Base32Hex), - ("base16", Format::Base16), - ("base2lsbf", Format::Base2Lsbf), - ("base2msbf", Format::Base2Msbf), - ("z85", Format::Z85), +const ENCODINGS: &[(&str, Format, &str)] = &[ + ("base64", Format::Base64, "same as 'base64' program"), + ("base64url", Format::Base64Url, "file- and url-safe base64"), + ("base32", Format::Base32, "same as 'base32' program"), + ( + "base32hex", + Format::Base32Hex, + "extended hex alphabet base32", + ), + ("base16", Format::Base16, "hex encoding"), + ( + "base2lsbf", + Format::Base2Lsbf, + "bit string with least significant bit (lsb) first", + ), + ( + "base2msbf", + Format::Base2Msbf, + "bit string with most significant bit (msb) first", + ), + ( + "z85", + Format::Z85, + "ascii85-like encoding;\n\ + when encoding, input length must be a multiple of 4;\n\ + when decoding, input length must be a multiple of 5", + ), ]; pub fn uu_app() -> Command { @@ -41,6 +59,7 @@ pub fn uu_app() -> Command { command = command.arg( Arg::new(encoding.0) .long(encoding.0) + .help(encoding.2) .action(ArgAction::SetTrue), ); } diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 9bddd3d7ab3..5a8cec5ea60 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -159,8 +159,14 @@ where div_ceil(sz, options.output_bits), filename.display() ), - (_, true) => println!("{sum} {sz}"), - (_, false) => println!("{sum} {sz} {}", filename.display()), + (ALGORITHM_OPTIONS_CRC, true) => println!("{sum} {sz}"), + (ALGORITHM_OPTIONS_CRC, false) => println!("{sum} {sz} {}", filename.display()), + (ALGORITHM_OPTIONS_BLAKE2B, _) => println!("BLAKE2b ({}) = {sum}", filename.display()), + _ => println!( + "{} ({}) = {sum}", + options.algo_name.to_ascii_uppercase(), + filename.display() + ), } } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 3e69e5791b7..642bbcb145f 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1704,6 +1704,11 @@ fn copy_file( } copy_attributes(source, dest, &options.attributes)?; + if options.parents && should_preserve_attribute(options) { + for (x, y) in aligned_ancestors(source, dest) { + copy_attributes(x, y, &options.attributes)?; + } + } if let Some(progress_bar) = progress_bar { progress_bar.inc(fs::metadata(source)?.len()); @@ -1753,6 +1758,27 @@ fn copy_helper( Ok(()) } +fn should_preserve_attribute(options: &Options) -> bool { + let checks = [ + &options.attributes.mode, + &options.attributes.timestamps, + &options.attributes.links, + &options.attributes.context, + &options.attributes.xattr, + ]; + + #[cfg(unix)] + let checks = [ + checks.as_slice(), + [&options.attributes.ownership].as_slice(), + ] + .concat(); + + checks + .iter() + .any(|attr| matches!(attr, Preserve::Yes { .. })) +} + // "Copies" a FIFO by creating a new one. This workaround is because Rust's // built-in fs::copy does not handle FIFOs (see rust-lang/rust/issues/79390). #[cfg(unix)] diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index b81ef1a8efc..04b60dc3113 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -20,6 +20,9 @@ gcd = { workspace=true } libc = { workspace=true } uucore = { workspace=true, features=["memo"] } +[target.'cfg(any(target_os = "linux"))'.dependencies] +nix = { workspace=true, features = ["fs"] } + [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] signal-hook = { workspace=true } diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index e53b0b956a4..340b00c5ca4 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -5,7 +5,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized fadvise Fadvise FADV DONTNEED ESPIPE mod datastructures; use datastructures::*; @@ -42,9 +42,16 @@ use std::time; use clap::{crate_version, Arg, Command}; use gcd::Gcd; +#[cfg(target_os = "linux")] +use nix::{ + errno::Errno, + fcntl::{posix_fadvise, PosixFadviseAdvice}, +}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +#[cfg(target_os = "linux")] +use uucore::{show, show_if_err}; const ABOUT: &str = help_about!("dd.md"); const AFTER_HELP: &str = help_section!("after help", "dd.md"); @@ -133,6 +140,16 @@ impl Source { Self::StdinFile(f) } + /// The length of the data source in number of bytes. + /// + /// If it cannot be determined, then this function returns 0. + fn len(&self) -> std::io::Result { + match self { + Self::File(f) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), + _ => Ok(0), + } + } + fn skip(&mut self, n: u64) -> io::Result { match self { #[cfg(not(unix))] @@ -158,6 +175,23 @@ impl Source { Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()), } } + + /// Discard the system file cache for the given portion of the data source. + /// + /// `offset` and `len` specify a contiguous portion of the data + /// source. This function informs the kernel that the specified + /// portion of the source is no longer needed. If not possible, + /// then this function returns an error. + #[cfg(target_os = "linux")] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> nix::Result<()> { + match self { + Self::File(f) => { + let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; + posix_fadvise(f.as_raw_fd(), offset, len, advice) + } + _ => Err(Errno::ESPIPE), // "Illegal seek" + } + } } impl Read for Source { @@ -298,6 +332,29 @@ impl<'a> Read for Input<'a> { } impl<'a> Input<'a> { + /// Discard the system file cache for the given portion of the input. + /// + /// `offset` and `len` specify a contiguous portion of the input. + /// This function informs the kernel that the specified portion of + /// the input file is no longer needed. If not possible, then this + /// function prints an error message to stderr and sets the exit + /// status code to 1. + #[allow(unused_variables)] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { + #[cfg(target_os = "linux")] + { + show_if_err!(self + .src + .discard_cache(offset, len) + .map_err_context(|| "failed to discard cache for: 'standard input'".to_string())); + } + #[cfg(not(target_os = "linux"))] + { + // TODO Is there a way to discard filesystem cache on + // these other operating systems? + } + } + /// Fills a given buffer. /// Reads in increments of 'self.ibs'. /// The start of each ibs-sized read follows the previous one. @@ -319,13 +376,13 @@ impl<'a> Input<'a> { _ => break, } } - buf.truncate(bytes_total); Ok(ReadStat { reads_complete, reads_partial, // Records are not truncated when filling. records_truncated: 0, + bytes_total: bytes_total.try_into().unwrap(), }) } @@ -336,6 +393,7 @@ impl<'a> Input<'a> { let mut reads_complete = 0; let mut reads_partial = 0; let mut base_idx = 0; + let mut bytes_total = 0; while base_idx < buf.len() { let next_blk = cmp::min(base_idx + self.settings.ibs, buf.len()); @@ -344,11 +402,13 @@ impl<'a> Input<'a> { match self.read(&mut buf[base_idx..next_blk])? { 0 => break, rlen if rlen < target_len => { + bytes_total += rlen; reads_partial += 1; let padding = vec![pad; target_len - rlen]; buf.splice(base_idx + rlen..next_blk, padding.into_iter()); } - _ => { + rlen => { + bytes_total += rlen; reads_complete += 1; } } @@ -361,6 +421,7 @@ impl<'a> Input<'a> { reads_complete, reads_partial, records_truncated: 0, + bytes_total: bytes_total.try_into().unwrap(), }) } } @@ -485,6 +546,33 @@ impl Dest { _ => Ok(()), } } + + /// Discard the system file cache for the given portion of the destination. + /// + /// `offset` and `len` specify a contiguous portion of the + /// destination. This function informs the kernel that the + /// specified portion of the destination is no longer needed. If + /// not possible, then this function returns an error. + #[cfg(target_os = "linux")] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> nix::Result<()> { + match self { + Self::File(f, _) => { + let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; + posix_fadvise(f.as_raw_fd(), offset, len, advice) + } + _ => Err(Errno::ESPIPE), // "Illegal seek" + } + } + + /// The length of the data destination in number of bytes. + /// + /// If it cannot be determined, then this function returns 0. + fn len(&self) -> std::io::Result { + match self { + Self::File(f, _) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), + _ => Ok(0), + } + } } /// Decide whether the given buffer is all zeros. @@ -627,6 +715,29 @@ impl<'a> Output<'a> { Ok(Self { dst, settings }) } + /// Discard the system file cache for the given portion of the output. + /// + /// `offset` and `len` specify a contiguous portion of the output. + /// This function informs the kernel that the specified portion of + /// the output file is no longer needed. If not possible, then + /// this function prints an error message to stderr and sets the + /// exit status code to 1. + #[allow(unused_variables)] + fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { + #[cfg(target_os = "linux")] + { + show_if_err!(self + .dst + .discard_cache(offset, len) + .map_err_context(|| "failed to discard cache for: 'standard output'".to_string())); + } + #[cfg(target_os = "linux")] + { + // TODO Is there a way to discard filesystem cache on + // these other operating systems? + } + } + /// Write the given bytes one block at a time. /// /// This may write partial blocks (for example, if the underlying @@ -721,6 +832,27 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { // Optimization: if no blocks are to be written, then don't // bother allocating any buffers. if let Some(Num::Blocks(0) | Num::Bytes(0)) = i.settings.count { + // Even though we are not reading anything from the input + // file, we still need to honor the `nocache` flag, which + // requests that we inform the system that we no longer + // need the contents of the input file in a system cache. + // + // TODO Better error handling for overflowing `len`. + if i.settings.iflags.nocache { + let offset = 0; + #[allow(clippy::useless_conversion)] + let len = i.src.len()?.try_into().unwrap(); + i.discard_cache(offset, len); + } + // Similarly, discard the system cache for the output file. + // + // TODO Better error handling for overflowing `len`. + if i.settings.oflags.nocache { + let offset = 0; + #[allow(clippy::useless_conversion)] + let len = o.dst.len()?.try_into().unwrap(); + o.discard_cache(offset, len); + } return finalize(&mut o, rstat, wstat, start, &prog_tx, output_thread); }; @@ -728,6 +860,13 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { // This is the max size needed. let mut buf = vec![BUF_INIT_BYTE; bsize]; + // Index in the input file where we are reading bytes and in + // the output file where we are writing bytes. + // + // These are updated on each iteration of the main loop. + let mut read_offset = 0; + let mut write_offset = 0; + // The main read/write loop. // // Each iteration reads blocks from the input and writes @@ -747,6 +886,30 @@ fn dd_copy(mut i: Input, mut o: Output) -> std::io::Result<()> { } let wstat_update = o.write_blocks(&buf)?; + // Discard the system file cache for the read portion of + // the input file. + // + // TODO Better error handling for overflowing `offset` and `len`. + let read_len = rstat_update.bytes_total; + if i.settings.iflags.nocache { + let offset = read_offset.try_into().unwrap(); + let len = read_len.try_into().unwrap(); + i.discard_cache(offset, len); + } + read_offset += read_len; + + // Discard the system file cache for the written portion + // of the output file. + // + // TODO Better error handling for overflowing `offset` and `len`. + let write_len = wstat_update.bytes_total; + if o.settings.oflags.nocache { + let offset = write_offset.try_into().unwrap(); + let len = write_len.try_into().unwrap(); + o.discard_cache(offset, len); + } + write_offset += write_len; + // Update the read/write stats and inform the progress thread once per second. // // If the receiver is disconnected, `send()` returns an diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index b9a3baf09aa..20a8da1ee7b 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -295,7 +295,7 @@ impl Parser { #[allow(clippy::cognitive_complexity)] fn parse_input_flags(&mut self, val: &str) -> Result<(), ParseError> { - let mut i = &mut self.iflag; + let i = &mut self.iflag; for f in val.split(',') { match f { // Common flags @@ -304,7 +304,7 @@ impl Parser { "directory" => linux_only!(f, i.directory = true), "dsync" => linux_only!(f, i.dsync = true), "sync" => linux_only!(f, i.sync = true), - "nocache" => return Err(ParseError::Unimplemented(f.to_string())), + "nocache" => linux_only!(f, i.nocache = true), "nonblock" => linux_only!(f, i.nonblock = true), "noatime" => linux_only!(f, i.noatime = true), "noctty" => linux_only!(f, i.noctty = true), @@ -327,7 +327,7 @@ impl Parser { #[allow(clippy::cognitive_complexity)] fn parse_output_flags(&mut self, val: &str) -> Result<(), ParseError> { - let mut o = &mut self.oflag; + let o = &mut self.oflag; for f in val.split(',') { match f { // Common flags @@ -336,7 +336,7 @@ impl Parser { "directory" => linux_only!(f, o.directory = true), "dsync" => linux_only!(f, o.dsync = true), "sync" => linux_only!(f, o.sync = true), - "nocache" => return Err(ParseError::Unimplemented(f.to_string())), + "nocache" => linux_only!(f, o.nocache = true), "nonblock" => linux_only!(f, o.nonblock = true), "noatime" => linux_only!(f, o.noatime = true), "noctty" => linux_only!(f, o.noctty = true), @@ -357,7 +357,7 @@ impl Parser { } fn parse_conv_flags(&mut self, val: &str) -> Result<(), ParseError> { - let mut c = &mut self.conv; + let c = &mut self.conv; for f in val.split(',') { match f { // Conversion diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index a135c3572da..54e17b882e2 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -55,7 +55,7 @@ fn unimplemented_flags_should_error() { let mut succeeded = Vec::new(); // The following flags are not implemented - for flag in ["cio", "nocache", "nolinks", "text", "binary"] { + for flag in ["cio", "nolinks", "text", "binary"] { let args = vec![format!("iflag={flag}")]; if Parser::new() diff --git a/src/uu/dd/src/progress.rs b/src/uu/dd/src/progress.rs index 51cfa92efd2..65af053b841 100644 --- a/src/uu/dd/src/progress.rs +++ b/src/uu/dd/src/progress.rs @@ -79,9 +79,9 @@ impl ProgUpdate { /// ```rust,ignore /// use std::io::Cursor; /// use std::time::Duration; - /// use crate::progress::{ProgUpdate, ReadState, WriteStat}; + /// use crate::progress::{ProgUpdate, ReadStat, WriteStat}; /// - /// let read_stat = ReadStat::new(1, 2, 3); + /// let read_stat = ReadStat::new(1, 2, 3, 999); /// let write_stat = WriteStat::new(4, 5, 6); /// let duration = Duration::new(789, 0); /// let prog_update = ProgUpdate { @@ -121,7 +121,7 @@ impl ProgUpdate { /// ```rust,ignore /// use std::io::Cursor; /// use std::time::Duration; - /// use crate::progress::{ProgUpdate, ReadState, WriteStat}; + /// use crate::progress::ProgUpdate; /// /// let prog_update = ProgUpdate { /// read_stat: Default::default(), @@ -191,7 +191,7 @@ impl ProgUpdate { /// ```rust,ignore /// use std::io::Cursor; /// use std::time::Duration; - /// use crate::progress::{ProgUpdate, ReadState, WriteStat}; + /// use crate::progress::ProgUpdate; /// /// let prog_update = ProgUpdate { /// read_stat: Default::default(), @@ -276,16 +276,20 @@ pub(crate) struct ReadStat { /// /// A truncated record can only occur in `conv=block` mode. pub(crate) records_truncated: u32, + + /// The total number of bytes read. + pub(crate) bytes_total: u64, } impl ReadStat { /// Create a new instance. #[allow(dead_code)] - fn new(complete: u64, partial: u64, truncated: u32) -> Self { + fn new(complete: u64, partial: u64, truncated: u32, bytes_total: u64) -> Self { Self { reads_complete: complete, reads_partial: partial, records_truncated: truncated, + bytes_total, } } @@ -315,6 +319,7 @@ impl std::ops::AddAssign for ReadStat { reads_complete: self.reads_complete + other.reads_complete, reads_partial: self.reads_partial + other.reads_partial, records_truncated: self.records_truncated + other.records_truncated, + bytes_total: self.bytes_total + other.bytes_total, } } } @@ -514,7 +519,7 @@ mod tests { #[test] fn test_read_stat_report() { - let read_stat = ReadStat::new(1, 2, 3); + let read_stat = ReadStat::new(1, 2, 3, 4); let mut cursor = Cursor::new(vec![]); read_stat.report(&mut cursor).unwrap(); assert_eq!(cursor.get_ref(), b"1+2 records in\n"); @@ -530,7 +535,7 @@ mod tests { #[test] fn test_prog_update_write_io_lines() { - let read_stat = ReadStat::new(1, 2, 3); + let read_stat = ReadStat::new(1, 2, 3, 4); let write_stat = WriteStat::new(4, 5, 6); let duration = Duration::new(789, 0); let complete = false; diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index db8024c2e79..9219876879f 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -41,7 +41,13 @@ use unicode_width::UnicodeWidthStr; target_os = "linux", target_os = "macos", target_os = "android", - target_os = "ios" + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" ))] use uucore::libc::{dev_t, major, minor}; #[cfg(unix)] @@ -2716,7 +2722,13 @@ fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId { target_os = "linux", target_os = "macos", target_os = "android", - target_os = "ios" + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" ))] { let ft = metadata.file_type(); diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index d1f82bf885e..5bc660d0874 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -18,7 +18,7 @@ path = "src/mv.rs" clap = { workspace=true } fs_extra = { workspace=true } indicatif = { workspace=true } -uucore = { workspace=true } +uucore = { workspace=true, features=["fs"] } [[bin]] name = "mv" diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index ed648186372..831b362ae71 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -25,6 +25,7 @@ use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError, UUsageError}; +use uucore::fs::are_hardlinks_to_same_file; use uucore::update_control::{self, UpdateMode}; use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show}; @@ -237,93 +238,90 @@ fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode { } } -fn exec(files: &[OsString], b: &Behavior) -> UResult<()> { - let paths: Vec = { - let paths = files.iter().map(Path::new); - - // Strip slashes from path, if strip opt present - if b.strip_slashes { - paths - .map(|p| p.components().as_path().to_owned()) - .collect::>() - } else { - paths.map(|p| p.to_owned()).collect::>() - } - }; +fn parse_paths(files: &[OsString], b: &Behavior) -> Vec { + let paths = files.iter().map(Path::new); - if let Some(ref name) = b.target_dir { - return move_files_into_dir(&paths, &PathBuf::from(name), b); + if b.strip_slashes { + paths + .map(|p| p.components().as_path().to_owned()) + .collect::>() + } else { + paths.map(|p| p.to_owned()).collect::>() } - match paths.len() { - /* case 0/1 are not possible thanks to clap */ - 2 => { - let source = &paths[0]; - let target = &paths[1]; - // Here we use the `symlink_metadata()` method instead of `exists()`, - // since it handles dangling symlinks correctly. The method gives an - // `Ok()` results unless the source does not exist, or the user - // lacks permission to access metadata. - if source.symlink_metadata().is_err() { - return Err(MvError::NoSuchFile(source.quote().to_string()).into()); - } +} - // GNU semantics are: if the source and target are the same, no move occurs and we print an error - if source.eq(target) { - // Done to match GNU semantics for the dot file - if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() { - return Err(MvError::SameFile( - source.quote().to_string(), - target.quote().to_string(), - ) - .into()); - } else { - return Err(MvError::SelfSubdirectory(source.display().to_string()).into()); - } - } +fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> { + if source.symlink_metadata().is_err() { + return Err(MvError::NoSuchFile(source.quote().to_string()).into()); + } - if target.is_dir() { - if b.no_target_dir { - if source.is_dir() { - rename(source, target, b, None).map_err_context(|| { - format!("cannot move {} to {}", source.quote(), target.quote()) - }) - } else { - Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) - } - } else { - move_files_into_dir(&[source.clone()], target, b) - } - } else if target.exists() && source.is_dir() { - match b.overwrite { - OverwriteMode::NoClobber => return Ok(()), - OverwriteMode::Interactive => { - if !prompt_yes!("overwrite {}? ", target.quote()) { - return Err(io::Error::new(io::ErrorKind::Other, "").into()); - } - } - OverwriteMode::Force => {} - }; - Err(MvError::NonDirectoryToDirectory( - source.quote().to_string(), - target.quote().to_string(), - ) - .into()) + if (source.eq(target) || are_hardlinks_to_same_file(source, target)) + && b.backup != BackupMode::SimpleBackup + { + if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() { + return Err( + MvError::SameFile(source.quote().to_string(), target.quote().to_string()).into(), + ); + } else { + return Err(MvError::SelfSubdirectory(source.display().to_string()).into()); + } + } + + if target.is_dir() { + if b.no_target_dir { + if source.is_dir() { + rename(source, target, b, None).map_err_context(|| { + format!("cannot move {} to {}", source.quote(), target.quote()) + }) } else { - rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{e}"))) + Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) } + } else { + move_files_into_dir(&[source.to_path_buf()], target, b) } - _ => { - if b.no_target_dir { - return Err(UUsageError::new( - 1, - format!("mv: extra operand {}", files[2].quote()), - )); + } else if target.exists() && source.is_dir() { + match b.overwrite { + OverwriteMode::NoClobber => return Ok(()), + OverwriteMode::Interactive => { + if !prompt_yes!("overwrite {}? ", target.quote()) { + return Err(io::Error::new(io::ErrorKind::Other, "").into()); + } } - let target_dir = paths.last().unwrap(); - let sources = &paths[..paths.len() - 1]; + OverwriteMode::Force => {} + }; + Err(MvError::NonDirectoryToDirectory( + source.quote().to_string(), + target.quote().to_string(), + ) + .into()) + } else { + rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{e}"))) + } +} - move_files_into_dir(sources, target_dir, b) - } +fn handle_multiple_paths(paths: &[PathBuf], b: &Behavior) -> UResult<()> { + if b.no_target_dir { + return Err(UUsageError::new( + 1, + format!("mv: extra operand {}", paths[2].quote()), + )); + } + let target_dir = paths.last().unwrap(); + let sources = &paths[..paths.len() - 1]; + + move_files_into_dir(sources, target_dir, b) +} + +fn exec(files: &[OsString], b: &Behavior) -> UResult<()> { + let paths = parse_paths(files, b); + + if let Some(ref name) = b.target_dir { + return move_files_into_dir(&paths, &PathBuf::from(name), b); + } + + match paths.len() { + 2 => handle_two_paths(&paths[0], &paths[1], b), + _ => handle_multiple_paths(&paths, b), } } diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 50d83a67cbd..0d905691563 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -18,7 +18,7 @@ path = "src/tac.rs" [dependencies] memchr = { workspace=true } -memmap2 = "0.5" +memmap2 = "0.6" regex = { workspace=true } clap = { workspace=true } uucore = { workspace=true } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 2df32b6145e..8693fbfc4f6 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -32,7 +32,7 @@ time = { workspace=true, optional=true, features = ["formatting", "local-offset" data-encoding = { version="2.3", optional=true } data-encoding-macro = { version="0.1.12", optional=true } z85 = { version="3.0.5", optional=true } -libc = { version="0.2.143", optional=true } +libc = { version="0.2.144", optional=true } once_cell = { workspace=true } os_display = "0.1.3" diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 4de07f50273..797be9c2cdb 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -617,14 +617,47 @@ pub fn is_symlink_loop(path: &Path) -> bool { false } +#[cfg(not(unix))] +// Hard link comparison is not supported on non-Unix platforms +pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool { + false +} + +/// Checks if two paths are hard links to the same file. +/// +/// # Arguments +/// +/// * `source` - A reference to a `Path` representing the source path. +/// * `target` - A reference to a `Path` representing the target path. +/// +/// # Returns +/// +/// * `bool` - Returns `true` if the paths are hard links to the same file, and `false` otherwise. +#[cfg(unix)] +pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool { + let source_metadata = match fs::metadata(source) { + Ok(metadata) => metadata, + Err(_) => return false, + }; + + let target_metadata = match fs::metadata(target) { + Ok(metadata) => metadata, + Err(_) => return false, + }; + + source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. use super::*; #[cfg(unix)] + use std::io::Write; + #[cfg(unix)] use std::os::unix; #[cfg(unix)] - use tempfile::tempdir; + use tempfile::{tempdir, NamedTempFile}; struct NormalizePathTestCase<'a> { path: &'a str, @@ -769,4 +802,44 @@ mod tests { assert!(is_symlink_loop(&symlink1_path)); } + + #[cfg(unix)] + #[test] + fn test_are_hardlinks_to_same_file_same_file() { + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!(temp_file, "Test content").unwrap(); + + let path1 = temp_file.path(); + let path2 = temp_file.path(); + + assert_eq!(are_hardlinks_to_same_file(&path1, &path2), true); + } + + #[cfg(unix)] + #[test] + fn test_are_hardlinks_to_same_file_different_files() { + let mut temp_file1 = NamedTempFile::new().unwrap(); + writeln!(temp_file1, "Test content 1").unwrap(); + + let mut temp_file2 = NamedTempFile::new().unwrap(); + writeln!(temp_file2, "Test content 2").unwrap(); + + let path1 = temp_file1.path(); + let path2 = temp_file2.path(); + + assert_eq!(are_hardlinks_to_same_file(&path1, &path2), false); + } + + #[cfg(unix)] + #[test] + fn test_are_hardlinks_to_same_file_hard_link() { + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!(temp_file, "Test content").unwrap(); + let path1 = temp_file.path(); + + let path2 = temp_file.path().with_extension("hardlink"); + fs::hard_link(&path1, &path2).unwrap(); + + assert_eq!(are_hardlinks_to_same_file(&path1, &path2), true); + } } diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 814d4c03a68..28c978ba6d9 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -1,7 +1,11 @@ -// spell-checker:ignore (words) asdf +// spell-checker:ignore (words) asdf algo algos use crate::common::util::TestScenario; +const ALGOS: [&str; 11] = [ + "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", "sm3", +]; + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); @@ -12,7 +16,7 @@ fn test_single_file() { new_ucmd!() .arg("lorem_ipsum.txt") .succeeds() - .stdout_is_fixture("single_file.expected"); + .stdout_is_fixture("crc_single_file.expected"); } #[test] @@ -21,7 +25,7 @@ fn test_multiple_files() { .arg("lorem_ipsum.txt") .arg("alice_in_wonderland.txt") .succeeds() - .stdout_is_fixture("multiple_files.expected"); + .stdout_is_fixture("crc_multiple_files.expected"); } #[test] @@ -29,11 +33,11 @@ fn test_stdin() { new_ucmd!() .pipe_in_fixture("lorem_ipsum.txt") .succeeds() - .stdout_is_fixture("stdin.expected"); + .stdout_is_fixture("crc_stdin.expected"); } #[test] -fn test_empty() { +fn test_empty_file() { let (at, mut ucmd) = at_and_ucmd!(); at.touch("a"); @@ -62,25 +66,26 @@ fn test_arg_overrides_stdin() { } #[test] -fn test_invalid_file() { - let ts = TestScenario::new(util_name!()); - let at = ts.fixtures.clone(); - - let folder_name = "asdf"; +fn test_nonexisting_file() { + let file_name = "asdf"; - // First check when file doesn't exist - ts.ucmd() - .arg(folder_name) + new_ucmd!() + .arg(file_name) .fails() .no_stdout() - .stderr_contains("cksum: asdf: No such file or directory"); + .stderr_contains(format!("cksum: {file_name}: No such file or directory")); +} + +#[test] +fn test_folder() { + let (at, mut ucmd) = at_and_ucmd!(); - // Then check when the file is of an invalid type + let folder_name = "a_folder"; at.mkdir(folder_name); - ts.ucmd() - .arg(folder_name) + + ucmd.arg(folder_name) .succeeds() - .stdout_only("4294967295 0 asdf\n"); + .stdout_only(format!("4294967295 0 {folder_name}\n")); } // Make sure crc is correct for files larger than 32 bytes @@ -116,77 +121,41 @@ fn test_stdin_larger_than_128_bytes() { } #[test] -fn test_sha1_single_file() { - new_ucmd!() - .arg("-a=sha1") - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is("ab1dd0bae1d8883a3d18a66de6afbd28252cfbef 772 lorem_ipsum.txt\n"); -} - -#[test] -fn test_sm3_single_file() { - new_ucmd!() - .arg("-a=sm3") - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_is( - "6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 772 lorem_ipsum.txt\n", - ); -} - -#[test] -fn test_bsd_single_file() { - new_ucmd!() - .arg("-a=bsd") - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_only_fixture("bsd_single_file.expected"); -} - -#[test] -fn test_bsd_multiple_files() { - new_ucmd!() - .arg("-a=bsd") - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_only_fixture("bsd_multiple_files.expected"); -} - -#[test] -fn test_bsd_stdin() { - new_ucmd!() - .arg("-a=bsd") - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_only_fixture("bsd_stdin.expected"); -} - -#[test] -fn test_sysv_single_file() { - new_ucmd!() - .arg("-a=sysv") - .arg("lorem_ipsum.txt") - .succeeds() - .stdout_only_fixture("sysv_single_file.expected"); -} - -#[test] -fn test_sysv_multiple_files() { - new_ucmd!() - .arg("-a=sysv") - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .succeeds() - .stdout_only_fixture("sysv_multiple_files.expected"); -} - -#[test] -fn test_sysv_stdin() { - new_ucmd!() - .arg("-a=sysv") - .pipe_in_fixture("lorem_ipsum.txt") - .succeeds() - .stdout_only_fixture("sysv_stdin.expected"); +fn test_algorithm_single_file() { + for algo in ALGOS { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_single_file.expected")); + } + } +} + +#[test] +fn test_algorithm_multiple_files() { + for algo in ALGOS { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_multiple_files.expected")); + } + } +} + +#[test] +fn test_algorithm_stdin() { + for algo in ALGOS { + for option in ["-a", "--algorithm"] { + new_ucmd!() + .arg(format!("{option}={algo}")) + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture(format!("{algo}_stdin.expected")); + } + } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 0a6e5804973..86fbf9dcc0a 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1075,6 +1075,46 @@ fn test_cp_parents_dest_not_directory() { .stderr_contains("with --parents, the destination must be a directory"); } +#[test] +fn test_cp_parents_with_permissions_copy_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + let dir = "dir"; + let file = "p1/p2/file"; + + at.mkdir(dir); + at.mkdir_all("p1/p2"); + at.touch(file); + + let p1_mode = 0o0777; + let p2_mode = 0o0711; + let file_mode = 0o0702; + + #[cfg(unix)] + { + at.set_mode("p1", p1_mode); + at.set_mode("p1/p2", p2_mode); + at.set_mode(file, file_mode); + } + + ucmd.arg("-p") + .arg("--parents") + .arg(file) + .arg(dir) + .succeeds(); + + #[cfg(all(unix, not(target_os = "freebsd")))] + { + let p1_metadata = at.metadata("p1"); + let p2_metadata = at.metadata("p1/p2"); + let file_metadata = at.metadata(file); + + assert_metadata_eq!(p1_metadata, at.metadata("dir/p1")); + assert_metadata_eq!(p2_metadata, at.metadata("dir/p1/p2")); + assert_metadata_eq!(file_metadata, at.metadata("dir/p1/p2/file")); + } +} + #[test] #[cfg(unix)] fn test_cp_writable_special_file_permissions() { @@ -1581,13 +1621,13 @@ fn test_cp_one_file_system() { use walkdir::WalkDir; let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; // Test must be run as root (or with `sudo -E`) if scene.cmd("whoami").run().stdout_str() != "root\n" { return; } - let at = scene.fixtures.clone(); let at_src = AtPath::new(&at.plus(TEST_MOUNT_COPY_FROM_FOLDER)); let at_dst = AtPath::new(&at.plus(TEST_COPY_TO_FOLDER_NEW)); diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index edad87bd6c0..42a94099db6 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -282,6 +282,27 @@ fn test_date_for_invalid_file() { } #[test] +#[cfg(unix)] +fn test_date_for_no_permission_file() { + let (at, mut ucmd) = at_and_ucmd!(); + const FILE: &str = "file-no-perm-1"; + + use std::os::unix::fs::PermissionsExt; + let file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .open(at.plus(FILE)) + .unwrap(); + file.set_permissions(std::fs::Permissions::from_mode(0o222)) + .unwrap(); + let result = ucmd.arg("--file").arg(FILE).fails(); + result.no_stdout(); + assert_eq!( + result.stderr_str().trim(), + format!("date: {FILE}: Permission denied") + ); +} + fn test_date_for_dir_as_file() { let result = new_ucmd!().arg("--file").arg("/").fails(); result.no_stdout(); diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 208869dc84e..fccd0604648 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1,7 +1,7 @@ // spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg use crate::common::util::TestScenario; -#[cfg(not(windows))] +#[cfg(all(not(windows), feature = "printf"))] use crate::common::util::{UCommand, TESTS_BINARY}; use regex::Regex; @@ -1549,3 +1549,29 @@ fn test_multiple_processes_writing_stdout() { .succeeds() .stdout_only("bde\n"); } + +/// Test that discarding system file cache fails for stdin. +#[test] +#[cfg(target_os = "linux")] +fn test_nocache_stdin_error() { + #[cfg(not(target_env = "musl"))] + let detail = "Illegal seek"; + #[cfg(target_env = "musl")] + let detail = "Invalid seek"; + new_ucmd!() + .args(&["iflag=nocache", "count=0", "status=noxfer"]) + .fails() + .code_is(1) + .stderr_only(format!("dd: failed to discard cache for: 'standard input': {detail}\n0+0 records in\n0+0 records out\n")); +} + +/// Test for discarding system file cache. +#[test] +#[cfg(target_os = "linux")] +fn test_nocache_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("f", b"a".repeat(1 << 20).as_slice()); + ucmd.args(&["if=f", "of=/dev/null", "iflag=nocache", "status=noxfer"]) + .succeeds() + .stderr_only("2048+0 records in\n2048+0 records out\n"); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 84efb9daa44..d460952ed97 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1239,7 +1239,7 @@ fn test_ls_long_total_size() { ("long_si", "total 8.2k"), ] .iter() - .cloned() + .copied() .collect() } else { [ @@ -1248,7 +1248,7 @@ fn test_ls_long_total_size() { ("long_si", "total 2"), ] .iter() - .cloned() + .copied() .collect() }; @@ -3393,3 +3393,44 @@ fn test_tabsize_formatting() { .succeeds() .stdout_is("aaaaaaaa bbbb\ncccc dddddddd"); } + +#[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" +))] +#[test] +fn test_device_number() { + use std::fs::{metadata, read_dir}; + use std::os::unix::fs::{FileTypeExt, MetadataExt}; + use uucore::libc::{dev_t, major, minor}; + + let dev_dir = read_dir("/dev").unwrap(); + // let's use the first device for test + let blk_dev = dev_dir + .map(|res_entry| res_entry.unwrap()) + .find(|entry| { + entry.file_type().unwrap().is_block_device() + || entry.file_type().unwrap().is_char_device() + }) + .expect("Expect a block/char device"); + let blk_dev_path = blk_dev.path(); + let blk_dev_meta = metadata(blk_dev_path.as_path()).unwrap(); + let blk_dev_number = blk_dev_meta.rdev() as dev_t; + let (major, minor) = unsafe { (major(blk_dev_number), minor(blk_dev_number)) }; + let major_minor_str = format!("{}, {}", major, minor); + + let scene = TestScenario::new(util_name!()); + scene + .ucmd() + .arg("-l") + .arg(blk_dev_path.to_str().expect("should be UTF-8 encoded")) + .succeeds() + .stdout_contains(major_minor_str); +} diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 47cbd4a795a..5c1d6c747eb 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -400,6 +400,39 @@ fn test_mv_same_file() { .stderr_is(format!("mv: '{file_a}' and '{file_a}' are the same file\n",)); } +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_same_hardlink() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_same_file_a"; + let file_b = "test_mv_same_file_b"; + at.touch(file_a); + + at.hard_link(file_a, file_b); + + at.touch(file_a); + ucmd.arg(file_a) + .arg(file_b) + .fails() + .stderr_is(format!("mv: '{file_a}' and '{file_b}' are the same file\n",)); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_same_hardlink_backup_simple() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "test_mv_same_file_a"; + let file_b = "test_mv_same_file_b"; + at.touch(file_a); + + at.hard_link(file_a, file_b); + + ucmd.arg(file_a) + .arg(file_b) + .arg("--backup=simple") + .succeeds(); +} + #[test] fn test_mv_same_file_not_dot_dir() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index 5719e87d24b..076e72089a2 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -27,7 +27,7 @@ fn test_deleted_dir() { use std::process::Command; let ts = TestScenario::new(util_name!()); - let at = ts.fixtures.clone(); + let at = ts.fixtures; let output = Command::new("sh") .arg("-c") .arg(format!( diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 946c60d0ae6..51d552d6726 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -118,9 +118,10 @@ mod linux_only { use std::os::unix::io::FromRawFd; let mut fds: [c_int; 2] = [0, 0]; - if unsafe { libc::pipe(&mut fds as *mut c_int) } != 0 { - panic!("Failed to create pipe"); - } + assert!( + (unsafe { libc::pipe(&mut fds as *mut c_int) } == 0), + "Failed to create pipe" + ); // Drop the read end of the pipe let _ = unsafe { File::from_raw_fd(fds[0]) }; diff --git a/tests/by-util/test_yes.rs b/tests/by-util/test_yes.rs index 9f03780b741..c054a6e5f3a 100644 --- a/tests/by-util/test_yes.rs +++ b/tests/by-util/test_yes.rs @@ -6,12 +6,12 @@ use std::os::unix::process::ExitStatusExt; use crate::common::util::TestScenario; #[cfg(unix)] -fn check_termination(result: &ExitStatus) { +fn check_termination(result: ExitStatus) { assert_eq!(result.signal(), Some(libc::SIGPIPE)); } #[cfg(not(unix))] -fn check_termination(result: &ExitStatus) { +fn check_termination(result: ExitStatus) { assert!(result.success(), "yes did not exit successfully"); } @@ -23,7 +23,7 @@ fn run(args: &[&str], expected: &[u8]) { child.close_stdout(); #[allow(deprecated)] - check_termination(&child.wait_with_output().unwrap().status); + check_termination(child.wait_with_output().unwrap().status); assert_eq!(buf.as_slice(), expected); } diff --git a/tests/common/util.rs b/tests/common/util.rs index 5d72a7abfd2..0898a4ad762 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -2016,7 +2016,7 @@ impl UChild { /// Read, consume and return the output as [`String`] from [`Child`]'s stdout. /// - /// See also [`UChild::stdout_bytes] for side effects. + /// See also [`UChild::stdout_bytes`] for side effects. pub fn stdout(&mut self) -> String { String::from_utf8(self.stdout_bytes()).unwrap() } diff --git a/tests/fixtures/cksum/blake2b_multiple_files.expected b/tests/fixtures/cksum/blake2b_multiple_files.expected new file mode 100644 index 00000000000..97d06eb6fea --- /dev/null +++ b/tests/fixtures/cksum/blake2b_multiple_files.expected @@ -0,0 +1,2 @@ +BLAKE2b (lorem_ipsum.txt) = 0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 +BLAKE2b (alice_in_wonderland.txt) = 91b8b0f0868e905ad18b8ac35e4a1dacd289857b19258ab5d1e071761af758b0134ec152d4f011fe1825ca889c80c2e072ca70eb50548c25fc49a98937515af4 diff --git a/tests/fixtures/cksum/blake2b_single_file.expected b/tests/fixtures/cksum/blake2b_single_file.expected new file mode 100644 index 00000000000..64ede7ca040 --- /dev/null +++ b/tests/fixtures/cksum/blake2b_single_file.expected @@ -0,0 +1 @@ +BLAKE2b (lorem_ipsum.txt) = 0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 diff --git a/tests/fixtures/cksum/blake2b_stdin.expected b/tests/fixtures/cksum/blake2b_stdin.expected new file mode 100644 index 00000000000..15c34ea775a --- /dev/null +++ b/tests/fixtures/cksum/blake2b_stdin.expected @@ -0,0 +1 @@ +BLAKE2b (-) = 0e97a09189e560c3789c0bff1f020166861ef857d1fbfe4574de1842e3c06cabb9575e4af6309a166158c2b408d3c038c1b49d828b35158142cdc0396d1195c3 diff --git a/tests/fixtures/cksum/multiple_files.expected b/tests/fixtures/cksum/crc_multiple_files.expected similarity index 100% rename from tests/fixtures/cksum/multiple_files.expected rename to tests/fixtures/cksum/crc_multiple_files.expected diff --git a/tests/fixtures/cksum/single_file.expected b/tests/fixtures/cksum/crc_single_file.expected similarity index 100% rename from tests/fixtures/cksum/single_file.expected rename to tests/fixtures/cksum/crc_single_file.expected diff --git a/tests/fixtures/cksum/stdin.expected b/tests/fixtures/cksum/crc_stdin.expected similarity index 100% rename from tests/fixtures/cksum/stdin.expected rename to tests/fixtures/cksum/crc_stdin.expected diff --git a/tests/fixtures/cksum/md5_multiple_files.expected b/tests/fixtures/cksum/md5_multiple_files.expected new file mode 100644 index 00000000000..54023e7615f --- /dev/null +++ b/tests/fixtures/cksum/md5_multiple_files.expected @@ -0,0 +1,2 @@ +MD5 (lorem_ipsum.txt) = cd724690f7dc61775dfac400a71f2caa +MD5 (alice_in_wonderland.txt) = f6fa7033e16166a9589aa1c0388ffd58 diff --git a/tests/fixtures/cksum/md5_single_file.expected b/tests/fixtures/cksum/md5_single_file.expected new file mode 100644 index 00000000000..5975053d1e2 --- /dev/null +++ b/tests/fixtures/cksum/md5_single_file.expected @@ -0,0 +1 @@ +MD5 (lorem_ipsum.txt) = cd724690f7dc61775dfac400a71f2caa diff --git a/tests/fixtures/cksum/md5_stdin.expected b/tests/fixtures/cksum/md5_stdin.expected new file mode 100644 index 00000000000..b3b1090bccc --- /dev/null +++ b/tests/fixtures/cksum/md5_stdin.expected @@ -0,0 +1 @@ +MD5 (-) = cd724690f7dc61775dfac400a71f2caa diff --git a/tests/fixtures/cksum/sha1_multiple_files.expected b/tests/fixtures/cksum/sha1_multiple_files.expected new file mode 100644 index 00000000000..f20ea947153 --- /dev/null +++ b/tests/fixtures/cksum/sha1_multiple_files.expected @@ -0,0 +1,2 @@ +SHA1 (lorem_ipsum.txt) = ab1dd0bae1d8883a3d18a66de6afbd28252cfbef +SHA1 (alice_in_wonderland.txt) = 22b54b2520e8b4fa59eb10719028a4e587c12d1e diff --git a/tests/fixtures/cksum/sha1_single_file.expected b/tests/fixtures/cksum/sha1_single_file.expected new file mode 100644 index 00000000000..1b820559f61 --- /dev/null +++ b/tests/fixtures/cksum/sha1_single_file.expected @@ -0,0 +1 @@ +SHA1 (lorem_ipsum.txt) = ab1dd0bae1d8883a3d18a66de6afbd28252cfbef diff --git a/tests/fixtures/cksum/sha1_stdin.expected b/tests/fixtures/cksum/sha1_stdin.expected new file mode 100644 index 00000000000..ced9ce80d49 --- /dev/null +++ b/tests/fixtures/cksum/sha1_stdin.expected @@ -0,0 +1 @@ +SHA1 (-) = ab1dd0bae1d8883a3d18a66de6afbd28252cfbef diff --git a/tests/fixtures/cksum/sha224_multiple_files.expected b/tests/fixtures/cksum/sha224_multiple_files.expected new file mode 100644 index 00000000000..0d6b45b102d --- /dev/null +++ b/tests/fixtures/cksum/sha224_multiple_files.expected @@ -0,0 +1,2 @@ +SHA224 (lorem_ipsum.txt) = 3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 +SHA224 (alice_in_wonderland.txt) = 54c9c7d78458886418ce0845111fc49fe1c628ffd4bf3da14226ffd9 diff --git a/tests/fixtures/cksum/sha224_single_file.expected b/tests/fixtures/cksum/sha224_single_file.expected new file mode 100644 index 00000000000..a08d66bb4ed --- /dev/null +++ b/tests/fixtures/cksum/sha224_single_file.expected @@ -0,0 +1 @@ +SHA224 (lorem_ipsum.txt) = 3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 diff --git a/tests/fixtures/cksum/sha224_stdin.expected b/tests/fixtures/cksum/sha224_stdin.expected new file mode 100644 index 00000000000..1bd13869885 --- /dev/null +++ b/tests/fixtures/cksum/sha224_stdin.expected @@ -0,0 +1 @@ +SHA224 (-) = 3de66fbcad106e1b40ab391be56c51d2007eb1f9c655d0f4e29bfc01 diff --git a/tests/fixtures/cksum/sha256_multiple_files.expected b/tests/fixtures/cksum/sha256_multiple_files.expected new file mode 100644 index 00000000000..e6fd8beb520 --- /dev/null +++ b/tests/fixtures/cksum/sha256_multiple_files.expected @@ -0,0 +1,2 @@ +SHA256 (lorem_ipsum.txt) = f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 +SHA256 (alice_in_wonderland.txt) = 14ab7e5a0aa3a670222744714bc96961d51012cb216225d965db71824a46e5fe diff --git a/tests/fixtures/cksum/sha256_single_file.expected b/tests/fixtures/cksum/sha256_single_file.expected new file mode 100644 index 00000000000..e16abcb0f37 --- /dev/null +++ b/tests/fixtures/cksum/sha256_single_file.expected @@ -0,0 +1 @@ +SHA256 (lorem_ipsum.txt) = f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 diff --git a/tests/fixtures/cksum/sha256_stdin.expected b/tests/fixtures/cksum/sha256_stdin.expected new file mode 100644 index 00000000000..87bd8419531 --- /dev/null +++ b/tests/fixtures/cksum/sha256_stdin.expected @@ -0,0 +1 @@ +SHA256 (-) = f7c420501c50e00b309250100d67ea5e910981536b4582fe9c435bd92b3f1f02 diff --git a/tests/fixtures/cksum/sha384_multiple_files.expected b/tests/fixtures/cksum/sha384_multiple_files.expected new file mode 100644 index 00000000000..a5a3324a254 --- /dev/null +++ b/tests/fixtures/cksum/sha384_multiple_files.expected @@ -0,0 +1,2 @@ +SHA384 (lorem_ipsum.txt) = 4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb +SHA384 (alice_in_wonderland.txt) = b7966c97ef84ab5858db2e0cdd33fbaf4fa8346d84de65aba001e738c242598a43272854d0073ad1099404eaa1d93766 diff --git a/tests/fixtures/cksum/sha384_single_file.expected b/tests/fixtures/cksum/sha384_single_file.expected new file mode 100644 index 00000000000..8d673e60b06 --- /dev/null +++ b/tests/fixtures/cksum/sha384_single_file.expected @@ -0,0 +1 @@ +SHA384 (lorem_ipsum.txt) = 4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb diff --git a/tests/fixtures/cksum/sha384_stdin.expected b/tests/fixtures/cksum/sha384_stdin.expected new file mode 100644 index 00000000000..3c0d5c8189e --- /dev/null +++ b/tests/fixtures/cksum/sha384_stdin.expected @@ -0,0 +1 @@ +SHA384 (-) = 4be4b90a0d0d32966992921019f24abc824dcfb8b1c408102f1f6788fb80ba9a9a4c5a7b575a3353a90a8ee719481dcb diff --git a/tests/fixtures/cksum/sha512_multiple_files.expected b/tests/fixtures/cksum/sha512_multiple_files.expected new file mode 100644 index 00000000000..0f533b27c8f --- /dev/null +++ b/tests/fixtures/cksum/sha512_multiple_files.expected @@ -0,0 +1,2 @@ +SHA512 (lorem_ipsum.txt) = 965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 +SHA512 (alice_in_wonderland.txt) = 251646d5a7eb481e0f3aced7839d78dd5e97153f822dc55938e17059c485990d85d602e2881b528b565ab6262584a69c97b068b26bda81acc9356c53c7c1c96d diff --git a/tests/fixtures/cksum/sha512_single_file.expected b/tests/fixtures/cksum/sha512_single_file.expected new file mode 100644 index 00000000000..e9a02c96a0d --- /dev/null +++ b/tests/fixtures/cksum/sha512_single_file.expected @@ -0,0 +1 @@ +SHA512 (lorem_ipsum.txt) = 965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 diff --git a/tests/fixtures/cksum/sha512_stdin.expected b/tests/fixtures/cksum/sha512_stdin.expected new file mode 100644 index 00000000000..8321c4cc3e9 --- /dev/null +++ b/tests/fixtures/cksum/sha512_stdin.expected @@ -0,0 +1 @@ +SHA512 (-) = 965464ab2556aad58ebc73d89ad221e559797529ecafc0f466c11795cff6d6e2c60f96a07c542cfd1f426e5e4fe0a48aa15667ba44096b213d0813cd038dfa05 diff --git a/tests/fixtures/cksum/sm3_multiple_files.expected b/tests/fixtures/cksum/sm3_multiple_files.expected new file mode 100644 index 00000000000..eae2cde2f3d --- /dev/null +++ b/tests/fixtures/cksum/sm3_multiple_files.expected @@ -0,0 +1,2 @@ +SM3 (lorem_ipsum.txt) = 6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 +SM3 (alice_in_wonderland.txt) = d66617ae3c4e87828298dcd836f79efbab488c53b84e09c3e8e83a16c902418d diff --git a/tests/fixtures/cksum/sm3_single_file.expected b/tests/fixtures/cksum/sm3_single_file.expected new file mode 100644 index 00000000000..cf4b8304a3d --- /dev/null +++ b/tests/fixtures/cksum/sm3_single_file.expected @@ -0,0 +1 @@ +SM3 (lorem_ipsum.txt) = 6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2 diff --git a/tests/fixtures/cksum/sm3_stdin.expected b/tests/fixtures/cksum/sm3_stdin.expected new file mode 100644 index 00000000000..436fcfb4142 --- /dev/null +++ b/tests/fixtures/cksum/sm3_stdin.expected @@ -0,0 +1 @@ +SM3 (-) = 6d296b805d060bfed22808df308dbb9b4317794dd4ed6740a10770a782699bc2