From 9c3117af2785fe3ccdbdf7743e773b443a70dbf5 Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Sun, 20 Sep 2026 12:33:01 +0200 Subject: [PATCH] mv: keep copying xattrs after one fails on a cross-device move --- .github/workflows/CICD.yml | 2 +- .github/workflows/GnuComment.yml | 8 +- .github/workflows/make.yml | 19 +- Cargo.lock | 1 - GNUmakefile | 9 - build.rs | 1 - deny.toml | 2 + fuzz/fuzz_targets/fuzz_env.rs | 1 - src/uu/base32/src/base_common.rs | 1 - src/uu/chroot/src/chroot.rs | 1 - src/uu/cp/src/cp.rs | 34 +-- src/uu/csplit/src/csplit.rs | 1 - src/uu/date/locales/en-US.ftl | 2 + src/uu/date/locales/fr-FR.ftl | 2 + src/uu/date/src/date.rs | 59 +++-- src/uu/env/src/split_iterator.rs | 3 +- src/uu/expr/src/locale_aware.rs | 2 +- src/uu/factor/src/factor.rs | 2 +- src/uu/fmt/src/fmt.rs | 4 +- src/uu/head/src/head.rs | 37 ++- src/uu/hostname/src/main.rs | 2 +- src/uu/ls/locales/en-US.ftl | 4 +- src/uu/ls/locales/fr-FR.ftl | 4 +- src/uu/ls/src/config.rs | 6 +- src/uu/ls/src/display.rs | 7 +- src/uu/ls/src/ls.rs | 30 ++- src/uu/mkfifo/src/mkfifo.rs | 10 +- src/uu/mktemp/src/mktemp.rs | 45 ++-- src/uu/mv/src/mv.rs | 10 +- src/uu/od/Cargo.toml | 6 - src/uu/od/benches/od_bench.rs | 52 ----- src/uu/od/src/od.rs | 87 +++---- src/uu/rm/src/platform/unix.rs | 4 +- src/uu/rm/src/rm.rs | 13 +- src/uu/shuf/benches/shuf_bench.rs | 14 +- src/uu/sort/src/chunks.rs | 3 +- src/uu/sort/src/sort.rs | 6 +- src/uu/stty/src/stty.rs | 11 +- src/uu/tail/src/chunks.rs | 13 +- src/uu/tsort/src/parser.rs | 2 +- src/uu/unexpand/locales/en-US.ftl | 2 +- src/uu/unexpand/locales/fr-FR.ftl | 2 +- src/uu/uptime/src/main.rs | 2 +- src/uu/users/Cargo.toml | 8 +- src/uu/users/src/users.rs | 9 - src/uu/whoami/src/main.rs | 2 +- src/uucore/Cargo.toml | 6 +- src/uucore/src/lib/features.rs | 6 +- src/uucore/src/lib/features/backup_control.rs | 4 +- src/uucore/src/lib/features/diagnostics.rs | 4 +- src/uucore/src/lib/features/fs.rs | 70 ++++-- src/uucore/src/lib/features/fsext/mod.rs | 4 +- src/uucore/src/lib/features/fsxattr.rs | 184 +++++++++++++-- src/uucore/src/lib/features/i18n/mod.rs | 5 +- src/uucore/src/lib/features/mode.rs | 4 +- .../src/lib/features/parser/num_parser.rs | 4 +- src/uucore/src/lib/features/perms.rs | 4 +- src/uucore/src/lib/features/process/unix.rs | 17 +- src/uucore/src/lib/features/ranges.rs | 18 +- src/uucore/src/lib/features/safe_traversal.rs | 11 +- src/uucore/src/lib/lib.rs | 9 +- src/uucore/src/lib/macros.rs | 4 +- src/uucore/src/lib/mods.rs | 2 +- src/uucore/src/lib/mods/error.rs | 4 +- src/uucore/src/lib/mods/locale.rs | 50 +--- tests/by-util/test_b2sum.rs | 4 +- tests/by-util/test_base64.rs | 12 - tests/by-util/test_basename.rs | 12 + tests/by-util/test_cp.rs | 96 +------- tests/by-util/test_date.rs | 123 ++++------ tests/by-util/test_expr.rs | 12 - tests/by-util/test_factor.rs | 1 - tests/by-util/test_fmt.rs | 30 --- tests/by-util/test_ls.rs | 67 +----- tests/by-util/test_md5sum.rs | 4 +- tests/by-util/test_mkfifo.rs | 2 +- tests/by-util/test_mv.rs | 213 ++++++++++++++++++ tests/by-util/test_od.rs | 19 -- tests/by-util/test_paste.rs | 1 - tests/by-util/test_rm.rs | 5 - tests/by-util/test_sha1sum.rs | 4 +- tests/by-util/test_sha224sum.rs | 4 +- tests/by-util/test_sha256sum.rs | 4 +- tests/by-util/test_sha384sum.rs | 4 +- tests/by-util/test_sha512sum.rs | 4 +- tests/by-util/test_sleep.rs | 3 +- tests/by-util/test_sort.rs | 1 - tests/by-util/test_tail.rs | 1 - tests/by-util/test_tee.rs | 4 +- tests/by-util/test_tr.rs | 23 +- tests/by-util/test_unexpand.rs | 4 +- tests/by-util/test_wc.rs | 3 +- util/fetch-gnu.sh | 12 +- util/gnu-patches/error_msg_uniq.diff | 6 +- util/gnu-patches/tests_comm.pl.patch | 6 +- util/gnu-patches/tests_env_env-S.pl.patch | 2 +- util/gnu-patches/tests_ls_no_cap.patch | 8 +- util/gnu-patches/tests_pwd-long.patch | 6 +- util/run-gnu-test.sh | 5 +- 99 files changed, 829 insertions(+), 831 deletions(-) delete mode 100644 src/uu/od/benches/od_bench.rs diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 49dc8daf63b..d434aa6524a 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -876,7 +876,7 @@ jobs: - { os: ubuntu-latest, features: "md5sum,sha1sum,sha224sum,sha256sum,sha384sum,sha512sum,cksum,openssl" } # macOS: vendored only — system libcrypto needs OPENSSL_DIR # pointing at Homebrew, which isn't worth wiring up for a smoke test. - # MinGW(UCRT): done at make.yml as actual consumer is MSYS2 only + # todo: check Windows (MinGW's system openssl) steps: - uses: actions/checkout@v7 with: diff --git a/.github/workflows/GnuComment.yml b/.github/workflows/GnuComment.yml index ed5deb0505c..8217888f46b 100644 --- a/.github/workflows/GnuComment.yml +++ b/.github/workflows/GnuComment.yml @@ -17,9 +17,7 @@ jobs: runs-on: ubuntu-latest if: > - github.event.workflow_run.event == 'pull_request' && - (github.event.workflow_run.conclusion == 'success' || - github.event.workflow_run.conclusion == 'failure') + github.event.workflow_run.event == 'pull_request' steps: - name: 'Download artifact' uses: actions/github-script@v9 @@ -36,10 +34,6 @@ jobs: var matchArtifact = artifacts.data.artifacts.filter((artifact) => { return artifact.name == "comment" })[0]; - if (!matchArtifact) { - core.info("No comment artifact found; skipping."); - return; - } var download = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index 8092f80038c..19a089bc60e 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -311,16 +311,15 @@ jobs: # Upgrading it mid-session changes the shared memory layout (60344 -> 59320), # which causes subsequent fork() calls to fail with EAGAIN (exit code 254). shell: 'C:\msys64\usr\bin\bash.exe --login -eo pipefail {0}' - # test Cygwin & MinGW(UCRT) instead of pre-installed Rust. run: | - pacman -Sy --noconfirm --needed base-devel libopenssl make openssl-devel pkgconf rust \ - mingw-w64-ucrt-x86_64-openssl mingw-w64-ucrt-x86_64-rust + pacman -Sy --noconfirm --needed base-devel libopenssl make openssl-devel pkgconf rust env: CHERE_INVOKING: 1 - name: "`(x86_64-pc-cygwin) make install PROG_PREFIX=uu- PROFILE=release-small COMPLETIONS=n MANPAGES=n LOCALES=n`" shell: 'C:\msys64\usr\bin\bash.exe --login -eo pipefail {0}' run: | set -x + export OPENSSL_NO_VENDOR=1 export CARGOFLAGS="--features openssl" DESTDIR=/tmp/c make install PROG_PREFIX=uu- PROFILE=release-small COMPLETIONS=n MANPAGES=n LOCALES=n # Check that cksum is linked with openssl (e.g. msys-crypto-*.dll) @@ -340,7 +339,6 @@ jobs: bsdtar -caf x86_64-pc-cygwin.tar.zst -C /tmp/c/usr/local bin env: CHERE_INVOKING: 1 - OPENSSL_NO_VENDOR: "1" RUST_BACKTRACE: "1" - name: Publish uses: softprops/action-gh-release@v3 @@ -353,21 +351,14 @@ jobs: individual-x86_64-pc-cygwin.tar.zst env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: "`make install` (MinGW UCRT)" - shell: 'C:\msys64\usr\bin\bash.exe --login -eo pipefail {0}' - env: - CHERE_INVOKING: 1 - MSYSTEM: "UCRT64" - OPENSSL_DIR: "/ucrt64" - OPENSSL_NO_VENDOR: "1" - OPENSSL_STATIC: "0" + - name: "`make install`" + shell: bash run: | set -x - export CARGOFLAGS="--features openssl" # Check that we exclude unix programs to avoid build failure # Windows's stdbuf depends on Cygwin's libstdbuf. So make should not try to install libstdbuf make install DESTDIR=/tmp/w PREFIX=/tmp/usr MULTICALL=y COMPLETIONS=n MANPAGES=n LOCALES=n \ - SKIP_UTILS="arch b2sum base32 base64 basename basenc cat comm cp csplit cut date dd df dir dircolors dirname du echo env expand expr factor false fmt fold head hostname hostid join link ln ls md5sum mkdir mktemp more mv nl nproc numfmt od paste pathchk pr printenv printf ptx pwd readlink realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum sha512sum shred shuf sleep sort split sum sync tac tail tee test touch tr true truncate tsort tty uname unexpand uniq unlink vdir wc whoami yes" + SKIP_UTILS="arch b2sum base32 base64 basename basenc cat cksum comm cp csplit cut date dd df dir dircolors dirname du echo env expand expr factor false fmt fold head hostname hostid join link ln ls md5sum mkdir mktemp more mv nl nproc numfmt od paste pathchk pr printenv printf ptx pwd readlink realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum sha512sum shred shuf sleep sort split sum sync tac tail tee test touch tr true truncate tsort tty uname unexpand uniq unlink vdir wc whoami yes" target/release/coreutils.exe stdbuf --version test_busybox: diff --git a/Cargo.lock b/Cargo.lock index daf02368116..9e49f968f76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3878,7 +3878,6 @@ name = "uu_od" version = "0.13.0" dependencies = [ "clap", - "codspeed-divan-compat", "fluent", "half", "libc", diff --git a/GNUmakefile b/GNUmakefile index 5ddf04ccb64..7de3aa6d995 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -261,15 +261,6 @@ INSTALLEES_WITH_EXTRA_LOCALE = \ $(INSTALLEES) \ $(if $(findstring sum, $(INSTALLEES)),checksum_common, ) install-locales: - @# Install common locales shared by all utilities - @if [ -d "$(BASEDIR)/src/uucore/locales" ]; then \ - $(INSTALL) -d "$(DESTDIR)$(DATAROOTDIR)/locales/uucore"; \ - for locale_file in "$(BASEDIR)"/src/uucore/locales/*.ftl; do \ - if [ "$$(basename "$$locale_file")" != "en-US.ftl" ]; then \ - $(INSTALL) -m 644 "$$locale_file" "$(DESTDIR)$(DATAROOTDIR)/locales/uucore/"; \ - fi; \ - done; \ - fi @# Install lazy error locales shared by all utilities @if [ -d "$(BASEDIR)/src/uucore/locales/errors" ]; then \ $(INSTALL) -d "$(DESTDIR)$(DATAROOTDIR)/locales/uucore/errors"; \ diff --git a/build.rs b/build.rs index 923b2f44d1a..e39f90ed2f6 100644 --- a/build.rs +++ b/build.rs @@ -21,7 +21,6 @@ pub fn main() { // Check for tldr.zip when building uudoc to warn users once at build time // instead of repeatedly at runtime for each utility - println!("cargo:rerun-if-changed=docs/tldr.zip"); if env::var("CARGO_FEATURE_UUDOC").is_ok() && !Path::new("docs/tldr.zip").exists() { println!( "cargo:warning=No tldr archive found, so the documentation will not include examples." diff --git a/deny.toml b/deny.toml index f0ded276c69..441916ff2ba 100644 --- a/deny.toml +++ b/deny.toml @@ -66,6 +66,8 @@ skip = [ { name = "itertools", version = "0.13.0" }, # codspeed-divan-compat-macros { name = "itertools", version = "0.14.0" }, + # ordered-multimap + { name = "hashbrown", version = "0.14.5" }, # lru (via num-prime) { name = "hashbrown", version = "0.16.1" }, # cexpr (via bindgen) diff --git a/fuzz/fuzz_targets/fuzz_env.rs b/fuzz/fuzz_targets/fuzz_env.rs index e7d81948dd4..62ab29c8c14 100644 --- a/fuzz/fuzz_targets/fuzz_env.rs +++ b/fuzz/fuzz_targets/fuzz_env.rs @@ -2,7 +2,6 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. - // spell-checker:ignore chdir putenv #![no_main] diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 4e58f5be599..9944dfd22b9 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -124,7 +124,6 @@ pub fn base_app(about: String, usage: String) -> Command { .short('w') .long(options::WRAP) .value_name("COLS") - .allow_hyphen_values(true) .help(translate!("base-common-help-wrap", "default" => WRAP_DEFAULT)) .overrides_with(options::WRAP), ) diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index dd7580b0906..4a092e218aa 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) NEWROOT Userspec chrooting chroots chdir pstatus repointed - mod error; use crate::error::ChrootError; diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 046ac6a8730..1d1467f4cf8 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -87,6 +87,10 @@ pub enum CpError { #[error("{}", translate!("cp-error-not-all-files-copied"))] NotAllFilesCopied, + /// Xattr copying already reported each failure; only the exit code is needed. + #[error("")] + XattrErrorsReported, + /// Simple [`walkdir::Error`] wrapper #[error("{0}")] WalkDirErr(#[from] walkdir::Error), @@ -1423,9 +1427,12 @@ fn show_error_if_needed(error: &CpError) { CpError::NotAllFilesCopied => { // Need to return an error code } - CpError::Skipped(_) => { + CpError::Skipped(_) | CpError::XattrErrorsReported => { // touch a b && echo "n"|cp -i a b && echo $? // should return an error + // XattrErrorsReported: each failing attribute was already + // reported on stderr by `copy_xattrs*`; only the exit code + // matters now. } // Format IoErrContext using strip_errno to remove "(os error N)" suffix // for GNU-compatible output @@ -1857,12 +1864,16 @@ fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyRe fs::set_permissions(dest, revert_perms)?; } - // If copying xattrs failed, propagate that error now with context. + // `copy_xattrs*` already reported each failure; add context only when xattrs are unsupported. copy_xattrs_result.map_err(|e| { - CpError::IoErrContext( - e, - translate!("cp-error-setting-attributes", "path" => dest.quote()), - ) + if uucore::fsxattr::is_xattr_unsupported(&e) { + CpError::IoErrContext( + e, + translate!("cp-error-setting-attributes", "path" => dest.quote()), + ) + } else { + CpError::XattrErrorsReported + } })?; Ok(()) @@ -1892,11 +1903,6 @@ pub(crate) fn copy_attributes( attributes.mode }; - // A created directory only defaults to copying the source mode; unlike an - // explicit preserve (-p/-a), GNU applies the umask to it. - let apply_umask_to_mode = - dest_is_freshly_created_dir && !matches!(attributes.mode, Preserve::Yes { .. }); - // Track whether `chown` to the source's uid succeeded. If it did not // (typical case: non-root user copying a root-owned setuid file), the // mode preservation below must strip setuid/setgid so the destination @@ -1961,12 +1967,6 @@ pub(crate) fn copy_attributes( let mode = perms.mode() & !0o6000; perms.set_mode(mode); } - if apply_umask_to_mode { - // The umask never covers setuid/setgid, so clear them - // explicitly: a non-preserving copy must not carry the - // source's set-user/group-ID bits into the new directory. - perms.set_mode(perms.mode() & !0o6000 & !uucore::mode::get_umask()); - } perms }; #[cfg(not(unix))] diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 7b18382696e..be32bfcfc8d 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore rustdoc - #![allow(rustdoc::private_intra_doc_links)] use std::borrow::Borrow; diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index a86e862aa1e..ea864904285 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -93,6 +93,7 @@ date-help-rfc-3339 = output date/time in RFC 3339 format. date-help-debug = annotate the parsed date, and warn about questionable usage to stderr date-help-reference = display the last modification time of FILE date-help-set = set time described by STRING +date-help-set-macos = set time described by STRING (not available on mac yet) date-help-set-redox = set time described by STRING (not available on redox yet) date-help-universal = print or set Coordinated Universal Time (UTC) @@ -100,6 +101,7 @@ date-error-invalid-date = invalid date '{$date}' date-error-invalid-format = invalid format '{$format}' ({$error}) date-error-expected-file-got-directory = expected file, got directory {$path} date-error-date-overflow = date overflow '{$date}' +date-error-setting-date-not-supported-macos = setting the date is not supported by macOS date-error-setting-date-not-supported-redox = setting the date is not supported by Redox date-error-cannot-set-date = cannot set date date-error-extra-operand = extra operand '{$operand}' diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 03aebbb089f..9a67704af1e 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -88,6 +88,7 @@ date-help-rfc-3339 = afficher la date/heure au format RFC 3339. date-help-debug = annoter la date analysée et avertir des usages douteux sur stderr date-help-reference = afficher l'heure de dernière modification du FICHIER date-help-set = définir l'heure décrite par CHAÎNE +date-help-set-macos = définir l'heure décrite par CHAÎNE (pas encore disponible sur mac) date-help-set-redox = définir l'heure décrite par CHAÎNE (pas encore disponible sur redox) date-help-universal = afficher ou définir le Temps Universel Coordonné (UTC) @@ -95,6 +96,7 @@ date-error-invalid-date = date invalide '{$date}' date-error-invalid-format = format invalide '{$format}' ({$error}) date-error-expected-file-got-directory = fichier attendu, répertoire obtenu {$path} date-error-date-overflow = débordement de date '{$date}' +date-error-setting-date-not-supported-macos = la définition de la date n'est pas prise en charge par macOS date-error-setting-date-not-supported-redox = la définition de la date n'est pas prise en charge par Redox date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 40115991218..455e72d6e09 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST foobarbaz unparseable -// spell-checker:ignore ohos OHOS tzdata tzdb tzif zoneinfo +// spell-checker:ignore ohos OHOS tzdata mod format_modifiers; mod locale; @@ -16,7 +16,6 @@ use jiff::{Timestamp, Zoned}; use parse_datetime::{ExtendedDateTime, ParsedDateTime}; use std::borrow::Cow; use std::collections::HashMap; -use std::ffi::OsString; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stderr}; use std::path::PathBuf; @@ -28,7 +27,6 @@ use uucore::error::{UError, UResult, USimpleError, strip_errno}; #[cfg(feature = "i18n-datetime")] use uucore::i18n::datetime::{localize_format_string, should_use_icu_locale}; use uucore::translate; -use uucore::translate_text; use uucore::{format_usage, show}; #[cfg(windows)] use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime}; @@ -38,7 +36,7 @@ use uucore::parser::shortcut_value_parser::ShortcutValueParser; /// OHOS helper: pass through the system time zone ID returned by /// TimeService (OH_TimeService_GetTimeZone, e.g. "Asia/Shanghai") and /// resolve it against the embedded IANA tzdata (jiff-tzdb) so that -/// historical DST rules and transitions are preserved. jiff's +/// historial DST rules and transitions are preserved. jiff's /// `try_system()` is useless on OHOS because both `/etc/localtime` and /// the zoneinfo dirs are absent. #[cfg(target_env = "ohos")] @@ -93,9 +91,9 @@ enum DateError { Write(std::io::Error), #[error("{}", translate!("date-error-extra-operand", "operand" => .operand))] ExtraOperand { operand: String }, - #[error("{}", translate_text!("date-error-invalid-date", "date" => .date))] + #[error("{}", translate!("date-error-invalid-date", "date" => .date))] InvalidDate { date: String }, - #[error("{}", translate_text!("date-error-format-missing-plus", "arg" => .arg))] + #[error("{}", translate!("date-error-format-missing-plus", "arg" => .arg))] FormatMissingPlus { arg: String }, #[error("{}", translate!("date-error-expected-file-got-directory", "path" => .path))] ExpectedFileGotDirectory { path: String }, @@ -103,6 +101,9 @@ enum DateError { CannotSetDate { path: String, error: String }, #[error("{}", translate!("date-error-invalid-format", "format" => .format, "error" => .error))] InvalidFormat { format: String, error: String }, + #[cfg(target_vendor = "apple")] + #[error("{}", translate!("date-error-setting-date-not-supported-macos"))] + SettingDateNotSupportedMacOs, #[cfg(target_os = "redox")] #[error("{}", translate!("date-error-setting-date-not-supported-redox"))] SettingDateNotSupportedRedox, @@ -115,6 +116,7 @@ struct Settings { utc: bool, format: Format, date_source: DateSource, + set_to: Option, debug: bool, } @@ -342,7 +344,7 @@ fn parse_military_timezone_with_offset(s: &str) -> Option<(i32, DayDelta)> { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - let date_source = if let Some(date_os) = matches.get_one::(OPT_DATE) { + let date_source = if let Some(date_os) = matches.get_one::(OPT_DATE) { // Convert OsString to String, handling invalid UTF-8 with GNU-compatible error let date = date_os.to_str().ok_or_else(|| { let bytes = date_os.as_encoded_bytes(); @@ -350,12 +352,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { USimpleError::new(1, format!("invalid date '{escaped_str}'")) })?; DateSource::Human(date.into()) - } else if let Some(file) = matches.get_one::(OPT_FILE) { - match file.as_encoded_bytes() { - b"-" => DateSource::Stdin, + } else if let Some(file) = matches.get_one::(OPT_FILE) { + match file.as_ref() { + "-" => DateSource::Stdin, _ => DateSource::File(file.into()), } - } else if let Some(file) = matches.get_one::(OPT_REFERENCE) { + } else if let Some(file) = matches.get_one::(OPT_REFERENCE) { DateSource::FileMtime(file.into()) } else if matches.get_flag(OPT_RESOLUTION) { DateSource::Resolution @@ -422,26 +424,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } }; - if let Some(input) = matches.get_one::(OPT_SET) { - match parse_date(input, &now, DebugOptions::new(debug_mode, true), false) { - Ok(ParsedDateTime::InRange(date)) => { - return set_system_datetime(convert_for_set(date, utc)); - } + let set_to = match matches.get_one::(OPT_SET) { + None => None, + Some(input) => match parse_date(input, &now, DebugOptions::new(debug_mode, true), false) { + Ok(ParsedDateTime::InRange(date)) => Some(date), Ok(ParsedDateTime::Extended(_)) | Err(_) => { return Err(Box::new(DateError::InvalidDate { date: input.clone(), })); } - } - } + }, + }; let settings = Settings { utc, format, date_source, + set_to, debug: debug_mode, }; + if let Some(date) = settings.set_to { + return set_system_datetime(convert_for_set(date, settings.utc)); + } + let allow_extended = matches!(settings.format, Format::Default); let output_time_zone = now.time_zone().clone(); @@ -686,7 +692,7 @@ pub fn uu_app() -> Command { .value_name("STRING") .allow_hyphen_values(true) .overrides_with(OPT_DATE) - .value_parser(clap::value_parser!(OsString)) + .value_parser(clap::value_parser!(std::ffi::OsString)) .help(translate!("date-help-date")), ) .arg( @@ -695,7 +701,6 @@ pub fn uu_app() -> Command { .long(OPT_FILE) .value_name("DATEFILE") .value_hint(clap::ValueHint::FilePath) - .value_parser(clap::value_parser!(OsString)) .conflicts_with(OPT_DATE) .help(translate!("date-help-file")), ) @@ -748,7 +753,6 @@ pub fn uu_app() -> Command { .long(OPT_REFERENCE) .value_name("FILE") .value_hint(clap::ValueHint::AnyPath) - .value_parser(clap::value_parser!(OsString)) .conflicts_with_all([OPT_DATE, OPT_FILE, OPT_RESOLUTION]) .overrides_with(OPT_REFERENCE) .help(translate!("date-help-reference")), @@ -760,10 +764,14 @@ pub fn uu_app() -> Command { .value_name("STRING") .allow_hyphen_values(true) .help({ - #[cfg(not(target_os = "redox"))] + #[cfg(not(any(target_vendor = "apple", target_os = "redox")))] { translate!("date-help-set") } + #[cfg(target_vendor = "apple")] + { + translate!("date-help-set-macos") + } #[cfg(target_os = "redox")] { translate!("date-help-set-redox") @@ -1301,12 +1309,17 @@ fn convert_for_set(date: Zoned, utc: bool) -> Zoned { } } +#[cfg(target_vendor = "apple")] +fn set_system_datetime(_date: Zoned) -> UResult<()> { + Err(Box::new(DateError::SettingDateNotSupportedMacOs)) +} + #[cfg(target_os = "redox")] fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(Box::new(DateError::SettingDateNotSupportedRedox)) } -#[cfg(all(unix, not(target_os = "redox")))] +#[cfg(all(unix, not(target_vendor = "apple"), not(target_os = "redox")))] /// System call to set date (unix). /// See here for more: /// `` diff --git a/src/uu/env/src/split_iterator.rs b/src/uu/env/src/split_iterator.rs index ec2b1d9d283..2cf3f8e20f3 100644 --- a/src/uu/env/src/split_iterator.rs +++ b/src/uu/env/src/split_iterator.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) Tomasz Miąsko rntfv FFFD varname - // This file is based on work from Tomasz Miąsko who published it as "shell_words" crate, // licensed under the Apache License, Version 2.0 // or the MIT license , at your option. @@ -16,6 +14,7 @@ //! Apart from the grammar differences, there is a new feature integrated: $VARIABLE expansion. //! //! [GNU env] +// spell-checker:ignore (words) Tomasz Miąsko rntfv FFFD varname #![forbid(unsafe_code)] diff --git a/src/uu/expr/src/locale_aware.rs b/src/uu/expr/src/locale_aware.rs index 52ddce4e917..9bb13e536e4 100644 --- a/src/uu/expr/src/locale_aware.rs +++ b/src/uu/expr/src/locale_aware.rs @@ -87,7 +87,7 @@ fn substr_with_locale( UEncoding::Utf8 => { // Create a buffer with the heuristic that all the chars are ASCII // and are 1-byte long. - let mut string = MaybeNonUtf8String::with_capacity(s.len().min(len)); + let mut string = MaybeNonUtf8String::with_capacity(len); let mut buf = [0; 4]; // Iterate on char-bytes, and skip them accordingly. diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index d6fa4ba8c5f..46af39c32e1 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -154,7 +154,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let print_exponents = matches.get_flag(options::EXPONENTS); let stdout = stdout(); - // use a smaller buffer here to pass a GNU test. + // We use a smaller buffer here to pass a gnu test. 4KiB appears to be the default pipe size for bash. let mut w = io::BufWriter::with_capacity(4 * 1024, stdout.lock()); if let Some(values) = matches.get_many::(options::NUMBER) { diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 6a34fe53e0b..b95c676f161 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -54,8 +54,6 @@ const DEFAULT_GOAL: usize = 70; const DEFAULT_WIDTH: usize = 75; // by default, goal is 93% of width const DEFAULT_GOAL_TO_WIDTH_RATIO: usize = 93; -// When only --goal is given, GNU sets the maximum width to goal + 10. -const DEFAULT_GOAL_WIDTH_SLACK: usize = 10; mod options { pub const CROWN_MARGIN: &str = "crown-margin"; @@ -151,7 +149,7 @@ impl FmtOptions { if g > DEFAULT_WIDTH { return Err(FmtError::GoalGreaterThanWidth.into()); } - let w = g + DEFAULT_GOAL_WIDTH_SLACK; + let w = (g * 100 / DEFAULT_GOAL_TO_WIDTH_RATIO).max(g + 3); (w, g) } (None, None) => (DEFAULT_WIDTH, DEFAULT_GOAL), diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index ae1ea58cf9f..cc7c94bd95f 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -84,7 +84,6 @@ impl Default for Mode { /// The message is built where it always was; the rest is what a caret needs: /// the value as typed, the option it was given to, and what the size parser /// made of it. -#[derive(Debug)] pub struct SizeError { pub message: String, option: OptionValue, @@ -173,7 +172,7 @@ fn arg_iterate<'a>( } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Default)] struct HeadOptions { pub quiet: bool, pub verbose: bool, @@ -184,21 +183,22 @@ struct HeadOptions { } impl HeadOptions { - /// Construct options from matches + ///Construct options from matches pub fn get_from(matches: &ArgMatches) -> Result { - let options = Self { - quiet: matches.get_flag(options::QUIET), - verbose: matches.get_flag(options::VERBOSE), - line_ending: LineEnding::from_zero_flag(matches.get_flag(options::ZERO)), - presume_input_pipe: matches.get_flag(options::PRESUME_INPUT_PIPE), - mode: Mode::from(matches)?, - // #[allow(clippy::unwrap_used, reason = "clap provides '-' by default")] - files: matches - .get_many::(options::FILES) - .unwrap() - .cloned() - .collect(), - }; + let mut options = Self::default(); + + options.quiet = matches.get_flag(options::QUIET); + options.verbose = matches.get_flag(options::VERBOSE); + options.line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); + options.presume_input_pipe = matches.get_flag(options::PRESUME_INPUT_PIPE); + + options.mode = Mode::from(matches)?; + // #[allow(clippy::unwrap_used, reason = "clap provides '-' by default")] + options.files = matches + .get_many::(options::FILES) + .unwrap() + .cloned() + .collect(); Ok(options) } @@ -650,14 +650,13 @@ mod tests { #[test] fn test_options_correct_defaults() { - let matches = uu_app().get_matches(); - let opts = HeadOptions::get_from(&matches).unwrap(); + let opts = HeadOptions::default(); assert!(!opts.verbose); assert!(!opts.quiet); assert_eq!(opts.line_ending, LineEnding::Newline); assert_eq!(opts.mode, Mode::FirstLines(10)); - assert_eq!(opts.files, vec!(OsString::from("-"))); + assert!(opts.files.is_empty()); } fn arg_outputs(src: &str) -> Result { diff --git a/src/uu/hostname/src/main.rs b/src/uu/hostname/src/main.rs index 3609547136e..7ad3364c79f 100644 --- a/src/uu/hostname/src/main.rs +++ b/src/uu/hostname/src/main.rs @@ -3,4 +3,4 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -uucore::bin!(uu_hostname, no_flush); +uucore::bin!(uu_hostname); diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index dda2379fa1a..acef1f0fb0f 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -133,8 +133,8 @@ ls-invalid-quoting-style = {$program}: Ignoring invalid value of environment var ls-invalid-columns-width = ignoring invalid width in environment variable COLUMNS: {$width} ls-invalid-ignore-pattern = Invalid pattern for ignore: {$pattern} ls-invalid-hide-pattern = Invalid pattern for hide: {$pattern} -ls-error-unrecognized-ls-colors-prefix = unrecognized prefix: {$prefix} -ls-error-unparsable-ls-colors = unparsable value for LS_COLORS environment variable +ls-warning-unrecognized-ls-colors-prefix = unrecognized prefix: {$prefix} +ls-warning-unparsable-ls-colors = unparsable value for LS_COLORS environment variable ls-total = total {$size} # Security context warnings diff --git a/src/uu/ls/locales/fr-FR.ftl b/src/uu/ls/locales/fr-FR.ftl index 1ee9d7dfaac..b7a891deb7d 100644 --- a/src/uu/ls/locales/fr-FR.ftl +++ b/src/uu/ls/locales/fr-FR.ftl @@ -131,6 +131,6 @@ ls-invalid-quoting-style = {$program} : Ignorer la valeur invalide de la variabl ls-invalid-columns-width = ignorer la largeur invalide dans la variable d'environnement COLUMNS : {$width} ls-invalid-ignore-pattern = Motif invalide pour ignore : {$pattern} ls-invalid-hide-pattern = Motif invalide pour hide : {$pattern} -ls-error-unrecognized-ls-colors-prefix = préfixe non reconnu : {$prefix} -ls-error-unparsable-ls-colors = valeur illisible pour la variable d'environnement LS_COLORS +ls-warning-unrecognized-ls-colors-prefix = préfixe non reconnu : {$prefix} +ls-warning-unparsable-ls-colors = valeur illisible pour la variable d'environnement LS_COLORS ls-total = total {$size} diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs index e187d211dab..e39e9851366 100644 --- a/src/uu/ls/src/config.rs +++ b/src/uu/ls/src/config.rs @@ -974,15 +974,15 @@ impl Config { if needs_color && let Err(err) = validate_ls_colors_env() { if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err { - show_error!( + show_warning!( "{}", translate!( - "ls-error-unrecognized-ls-colors-prefix", + "ls-warning-unrecognized-ls-colors-prefix", "prefix" => prefix.quote() ) ); } - show_error!("{}", translate!("ls-error-unparsable-ls-colors")); + show_warning!("{}", translate!("ls-warning-unparsable-ls-colors")); needs_color = false; } diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index fb0b366cf87..4d17562b614 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -162,13 +162,11 @@ enum SizeOrDeviceId { /// dir1: <- This as well /// file11 /// ``` -/// Returns the number of bytes the rendered name occupies, which `--dired` -/// needs to place the header in `//SUBDIRED//`. pub fn show_dir_name( path_data: &PathData, out: &mut BufWriter, config: &Config, -) -> std::io::Result { +) -> std::io::Result<()> { let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); let name = if config.hyperlink && !config.dired { @@ -178,8 +176,7 @@ pub fn show_dir_name( }; write_os_str(out, &name)?; - write!(out, ":")?; - Ok(name.len()) + write!(out, ":") } fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 45a5d29e139..3b8eec3fd38 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1078,23 +1078,29 @@ impl LsOutput for TextOutput<'_> { config: &Config, is_first: bool, ) -> UResult<()> { - if !is_first { + if is_first { + if config.dired { + dired::indent(&mut self.state.out)?; + } + show_dir_name(path_data, &mut self.state.out, config)?; + writeln!(self.state.out)?; + if config.dired { + let dir_len = path_data.path().as_os_str().len(); + dired::calculate_subdired(&mut self.dired, dir_len); + dired::add_dir_name(&mut self.dired, dir_len); + } + } else { writeln!(self.state.out)?; if config.dired { self.dired.line_offset += 1; // account for the blank line before recursive directory headings self.dired.padding = 0; + dired::indent(&mut self.state.out)?; + let dir_name_size = path_data.path().as_os_str().len(); + dired::calculate_subdired(&mut self.dired, dir_name_size); + dired::add_dir_name(&mut self.dired, dir_name_size); } - } - if config.dired { - dired::indent(&mut self.state.out)?; - } - let name_len = show_dir_name(path_data, &mut self.state.out, config)?; - writeln!(self.state.out)?; - if config.dired { - // The header is rendered with the quoting style in force, so the - // offsets must follow the rendered name, not the raw path. - dired::calculate_subdired(&mut self.dired, name_len); - dired::add_dir_name(&mut self.dired, name_len); + show_dir_name(path_data, &mut self.state.out, config)?; + writeln!(self.state.out)?; } Ok(()) } diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 0a94ea8a8b1..a28151cb443 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command, value_parser}; -use rustix::fs::{Mode, RawMode}; +use rustix::fs::Mode; use rustix::process::umask; use std::ffi::OsString; use uucore::display::Quotable; @@ -82,7 +82,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // attacker could use to swap the FIFO for a symlink between // mkfifo and chmod (issue #10020). let prev_umask = umask(Mode::empty()); - let mkfifo_result = create_fifo(f.as_str(), mode as RawMode); + let mkfifo_result = create_fifo(f.as_str(), mode); umask(prev_umask); if let Err(e) = mkfifo_result { @@ -154,13 +154,13 @@ pub fn uu_app() -> Command { // libc's path-based `mkfifo` there. Both rely on the caller having cleared // the umask so the requested mode is applied atomically (see issue #10020). #[cfg(not(target_vendor = "apple"))] -fn create_fifo(path: &str, mode: RawMode) -> std::io::Result<()> { +fn create_fifo(path: &str, mode: u32) -> std::io::Result<()> { use rustix::fs; - fs::mkfifoat(fs::CWD, path, Mode::from_bits_truncate(mode)).map_err(Into::into) + fs::mkfifoat(fs::CWD, path, Mode::from_bits_truncate(mode as fs::RawMode)).map_err(Into::into) } #[cfg(target_vendor = "apple")] -fn create_fifo(path: &str, mode: RawMode) -> std::io::Result<()> { +fn create_fifo(path: &str, mode: u32) -> std::io::Result<()> { use std::ffi::CString; let c_path = CString::new(path).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 9b7b5ca96da..887798c44a1 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -413,12 +413,37 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let path = mktemp(&options)?; + let dry_run = options.dry_run; + let suppress_file_err = options.quiet; + let make_dir = options.directory; + + // Parse file path parameters from the command-line options. + let Params { + directory: tmpdir, + prefix, + num_rand_chars: rand, + suffix, + } = Params::from(options)?; + + // Create the temporary file or directory, or simulate creating it. + let res = if dry_run { + Ok(dry_exec(&tmpdir, &prefix, rand, &suffix)) + } else { + exec(&tmpdir, &prefix, rand, &suffix, make_dir) + }; + + let res = if suppress_file_err { + // Mapping all UErrors to ExitCodes prevents the errors from being printed + res.map_err(|e| e.code().into()) + } else { + res + }; + let path = res?; if let Err(e) = println_verbatim(&path) { // The caller never learns the name, so leaving the file behind would // litter the temporary directory with something nothing can clean up. - if !options.dry_run { - let _ = if options.directory { + if !dry_run { + let _ = if make_dir { fs::remove_dir(&path) } else { fs::remove_file(&path) @@ -512,7 +537,7 @@ fn dry_exec(tmpdir: &Path, prefix: &str, rand: usize, suffix: &str) -> PathBuf { SmallRng::try_from_rng(&mut rngs::SysRng) .unwrap_or_else(|_| { //rand::rng panics if getrandom failed - SmallRng::seed_from_u64(bytes.as_ptr() as u64) + SmallRng::seed_from_u64(bytes.as_ptr() as usize as u64) }) .fill(bytes); for byte in bytes { @@ -634,8 +659,6 @@ fn get_tmpdir_env_or_default() -> PathBuf { /// Create a temporary file or directory /// /// Behavior is determined by the `options` parameter, see [`Options`] for details. -/// -/// The function is public so it can be used by nushell and others. pub fn mktemp(options: &Options) -> UResult { // Parse file path parameters from the command-line options. let Params { @@ -646,18 +669,10 @@ pub fn mktemp(options: &Options) -> UResult { } = Params::from(options.clone())?; // Create the temporary file or directory, or simulate creating it. - let res = if options.dry_run { + if options.dry_run { Ok(dry_exec(&tmpdir, &prefix, rand, &suffix)) } else { exec(&tmpdir, &prefix, rand, &suffix, options.directory) - }; - - if options.quiet { - // Only creation failures are silenced; a bad template is still reported. - // Mapping the UError to an ExitCode prevents the error from being printed. - res.map_err(|e| e.code().into()) - } else { - res } } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 3536c54a737..29f9f8abef4 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -1146,12 +1146,14 @@ fn rename_dir_fallback( display_manager, ); - // Apply xattrs using a file descriptor to avoid TOCTOU races, ignoring - // ENOTSUP/EOPNOTSUPP (filesystem without xattr support, which is expected - // for cross-device moves). + // Apply xattrs using a file descriptor to avoid TOCTOU races. // // The fd is opened read-only: a directory cannot be opened for writing, and // fsetxattr checks write permission on the inode, not the open mode. + // + // Per-attribute failures are reported by `apply_xattrs_fd_*` on stderr and + // must not fail the move. The source was already fully copied, so GNU mv + // completes the move and still exits 0. #[cfg(any( target_os = "freebsd", target_os = "hurd", @@ -1162,7 +1164,7 @@ fn rename_dir_fallback( { use std::fs::File; let dest = File::open(to)?; - fsxattr::apply_xattrs_fd_ignore_unsupported(&dest, xattrs)?; + let _ = fsxattr::apply_xattrs_fd_ignore_unsupported(&dest, xattrs); } result?; diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index 4f2193e62db..a52cd5b4ae5 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -25,9 +25,7 @@ libc.workspace = true rustix = { workspace = true, features = ["stdio"] } [dev-dependencies] -divan = { workspace = true } tempfile = { workspace = true } -uucore = { workspace = true, features = ["benchmark"] } [lints] workspace = true @@ -35,7 +33,3 @@ workspace = true [[bin]] name = "od" path = "src/main.rs" - -[[bench]] -name = "od_bench" -harness = false diff --git a/src/uu/od/benches/od_bench.rs b/src/uu/od/benches/od_bench.rs deleted file mode 100644 index 164272fc3ed..00000000000 --- a/src/uu/od/benches/od_bench.rs +++ /dev/null @@ -1,52 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use divan::{Bencher, black_box}; -use uu_od::uumain; -use uucore::benchmark::{get_bench_args, setup_test_file}; - -/// Size of the input dumped by each benchmark. od writes several bytes of -/// output per input byte, so this stays modest to keep the run times sane. -const INPUT_SIZE: usize = 1024 * 1024; - -/// Generate binary input covering the whole byte range, so that the printable -/// character lookups and the number formatting both see every possible value. -fn binary_data() -> Vec { - (0..INPUT_SIZE).map(|i| (i % 256) as u8).collect() -} - -/// Benchmark the default output format (octal 2-byte words). -#[divan::bench] -fn od_default(bencher: Bencher) { - let file_path = setup_test_file(&binary_data()); - - bencher - .with_inputs(|| get_bench_args(&[&file_path]).into_iter()) - .bench_values(|args| black_box(uumain(args))); -} - -/// Benchmark single byte hexadecimal output, the most common invocation. -#[divan::bench] -fn od_hex_bytes(bencher: Bencher) { - let file_path = setup_test_file(&binary_data()); - - bencher - .with_inputs(|| get_bench_args(&[&"-t", &"x1", &file_path]).into_iter()) - .bench_values(|args| black_box(uumain(args))); -} - -/// Benchmark named character output, which escapes control characters. -#[divan::bench] -fn od_chars(bencher: Bencher) { - let file_path = setup_test_file(&binary_data()); - - bencher - .with_inputs(|| get_bench_args(&[&"-c", &file_path]).into_iter()) - .bench_values(|args| black_box(uumain(args))); -} - -fn main() { - divan::main(); -} diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 1e59bf675a3..58ad1fdb18d 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -23,6 +23,7 @@ mod prn_float; mod prn_int; use std::cmp; +use std::fmt::Write; use std::io::{BufReader, Read}; use crate::byteorder_io::ByteOrder; @@ -694,33 +695,6 @@ fn extract_strings_from_input( } } -/// Writes `n` spaces to `writer` without allocating a string for them. -/// -/// The padding in front of an ascii dump grows with `-w`, which accepts huge -/// values, so it has to be written in chunks rather than materialized. -fn write_spaces(writer: &mut impl std::io::Write, n: usize) -> std::io::Result<()> { - const SPACES: [u8; 512] = [b' '; 512]; - - let mut remaining = n; - while remaining != 0 { - let chunk = cmp::min(remaining, SPACES.len()); - writer.write_all(&SPACES[..chunk])?; - remaining -= chunk; - } - Ok(()) -} - -/// Writes `s` to `writer`, adding its width in characters to `line_width`. -fn write_field( - writer: &mut impl std::io::Write, - s: &str, - line_width: &mut usize, -) -> std::io::Result<()> { - writer.write_all(s.as_bytes())?; - *line_width += s.chars().count(); - Ok(()) -} - /// Outputs a single line of input, into one or more lines human readable output. fn write_bytes( writer: &mut impl std::io::Write, @@ -730,49 +704,37 @@ fn write_bytes( ) -> std::io::Result<()> { let mut first = true; // First line of a multi-format raster. for f in output_info.spaced_formatters_iter() { - if first { - write!(writer, "{prefix}")?; // print offset - // if printing in multiple formats offset is printed only once - first = false; - } else { - // this takes the space of the file offset on subsequent - // lines of multi-format rasters. - write_spaces(writer, prefix.chars().count())?; - } + let mut output_text = String::new(); - // The formatted fields are written out as they are produced: a line - // holds up to `-w` bytes of input, so buffering it would allocate - // several times the width, which can be huge. - let mut line_width = 0; let mut b = 0; while b < input_decoder.length() { - let spacing = f.spacing[b % output_info.byte_size_block]; - write_spaces(writer, spacing)?; - line_width += spacing; + write!( + output_text, + "{:>width$}", + "", + width = f.spacing[b % output_info.byte_size_block] + ) + .unwrap(); match f.formatter_item_info.formatter { FormatWriter::IntWriter(func) => { let p = input_decoder.read_uint(b, f.formatter_item_info.byte_size); - write_field(writer, &func(p), &mut line_width)?; + output_text.push_str(&func(p)); } FormatWriter::FloatWriter(func) => { let p = input_decoder.read_float(b, f.formatter_item_info.byte_size); - write_field(writer, &func(p), &mut line_width)?; + output_text.push_str(&func(p)); } FormatWriter::LongDoubleWriter(func) => { let p = input_decoder.read_long_double(b); - write_field(writer, &func(p), &mut line_width)?; + output_text.push_str(&func(p)); } FormatWriter::BFloatWriter(func) => { let p = input_decoder.read_bfloat(b); - write_field(writer, &func(p), &mut line_width)?; + output_text.push_str(&func(p)); } FormatWriter::MultibyteWriter(func) => { - write_field( - writer, - &func(input_decoder.get_full_buffer(b)), - &mut line_width, - )?; + output_text.push_str(&func(input_decoder.get_full_buffer(b))); } } @@ -780,11 +742,24 @@ fn write_bytes( } if f.add_ascii_dump { - let missing_spacing = output_info.print_width_line.saturating_sub(line_width); - write_spaces(writer, missing_spacing + 2)?; - write!(writer, "{}", format_ascii_dump(input_decoder.get_buffer(0)))?; + let missing_spacing = output_info + .print_width_line + .saturating_sub(output_text.chars().count()); + output_text.extend(std::iter::repeat_n(' ', missing_spacing)); + output_text.push_str(" "); + output_text.push_str(&format_ascii_dump(input_decoder.get_buffer(0))); + } + + if first { + write!(writer, "{prefix}")?; // print offset + // if printing in multiple formats offset is printed only once + first = false; + } else { + // this takes the space of the file offset on subsequent + // lines of multi-format rasters. + write!(writer, "{:>width$}", "", width = prefix.chars().count())?; } - writeln!(writer)?; + writeln!(writer, "{output_text}")?; } Ok(()) } diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index 04f63929578..eef2874b44a 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -3,10 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fstatat unlinkat statx behaviour automount - // Unix-specific implementations for the rm utility +// spell-checker:ignore fstatat unlinkat statx behaviour automount + use indicatif::ProgressBar; use std::ffi::{OsStr, OsString}; use std::fs; diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 13e756e5825..eada6c3e34c 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -532,9 +532,7 @@ fn count_files_in_directory(p: &Path) -> u64 { entries .flatten() .map(|entry| match entry.file_type() { - Ok(ft) if ft.is_dir() && !ft.is_symlink() => { - count_files_in_directory(&entry.path()) - } + Ok(ft) if ft.is_dir() => count_files_in_directory(&entry.path()), Ok(_) => 1, Err(_) => 0, }) @@ -667,14 +665,7 @@ fn remove_dir_recursive( // a directory and we don't want to recurse. In particular, this // avoids an infinite recursion in the case of a link to the current // directory, like `ln -s . link`. - let metadata = match fs::symlink_metadata(path) { - Ok(metadata) => metadata, - Err(e) => return show_removal_error(e, path), - }; - if is_symlink_dir(&metadata) { - return remove_dir(path, options, progress_bar); - } - if !metadata.is_dir() || metadata.file_type().is_symlink() { + if !path.is_dir() || path.is_symlink() { return remove_file(path, options, progress_bar); } diff --git a/src/uu/shuf/benches/shuf_bench.rs b/src/uu/shuf/benches/shuf_bench.rs index 7f1036ce4fd..af37f09992a 100644 --- a/src/uu/shuf/benches/shuf_bench.rs +++ b/src/uu/shuf/benches/shuf_bench.rs @@ -9,9 +9,9 @@ use uucore::benchmark::{get_bench_args, setup_test_file, text_data}; /// Benchmark shuffling lines from a file /// Tests the default mode with a large number of lines -#[divan::bench(args = [(100_000, 80), (100_000, 10)])] -fn shuf_lines(bencher: Bencher, (num_lines, avg_line_length): (usize, usize)) { - let data = text_data::generate_by_lines(num_lines, avg_line_length); +#[divan::bench(args = [100_000])] +fn shuf_lines(bencher: Bencher, num_lines: usize) { + let data = text_data::generate_by_lines(num_lines, 80); let file_path = setup_test_file(&data); bencher @@ -32,11 +32,11 @@ fn shuf_input_range(bencher: Bencher, range_size: usize) { /// Benchmark shuffling with repeat (sampling with replacement) /// Tests the -r flag combined with -n to output a specific count -#[divan::bench(args = [(50_000, 80), (50_000, 10)])] -fn shuf_repeat_sampling(bencher: Bencher, (head_count, avg_line_length): (usize, usize)) { - let data = text_data::generate_by_lines(10_000, avg_line_length); +#[divan::bench(args = [50_000])] +fn shuf_repeat_sampling(bencher: Bencher, num_lines: usize) { + let data = text_data::generate_by_lines(10_000, 80); let file_path = setup_test_file(&data); - let count = format!("{head_count}"); + let count = format!("{num_lines}"); bencher .with_inputs(|| get_bench_args(&[&"-r", &"-n", &count, &file_path]).into_iter()) diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index ee8261dd856..ca9efcd13e8 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -3,10 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore ELEMS - //! Utilities for reading files as chunks. +// spell-checker:ignore ELEMS #![allow(dead_code)] // Ignores non-used warning for `borrow_buffer` in `Chunk` diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 5f86eea77bd..a6e88051893 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -3,13 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (misc) kKMGTPEZYRQ HFKJFK Mbdfhn getrlimit Nofile rlim bigdecimal extendedbigdecimal hexdigit behaviour keydef GETFD localeconv foldhash -// spell-checker:ignore (misc) uppercased qsort getmonth juin juil - // Although these links don't always seem to describe reality, check out the POSIX and GNU specs: // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sort.html // https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html +// spell-checker:ignore (misc) kKMGTPEZYRQ HFKJFK Mbdfhn getrlimit Nofile rlim bigdecimal extendedbigdecimal hexdigit behaviour keydef GETFD localeconv foldhash +// spell-checker:ignore (misc) uppercased qsort getmonth juin juil + mod buffer_hint; mod check; mod chunks; diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index d6e46d90916..dc1019ce1ab 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -650,13 +650,10 @@ fn print_terminal_size( // BSDs and Linux (not ppc/big-endian ppc64) use a u32 for the baud rate, so we can simply // print it. - #[cfg(any( - bsd, - all( - target_os = "linux", - not(target_arch = "powerpc"), - not(all(target_arch = "powerpc64", target_endian = "big")) - ) + #[cfg(any(target_os = "linux", bsd))] + #[cfg(all( + not(target_arch = "powerpc"), + not(all(target_arch = "powerpc64", target_endian = "big")) ))] printer.print(&translate!("stty-output-speed", "speed" => speed)); diff --git a/src/uu/tail/src/chunks.rs b/src/uu/tail/src/chunks.rs index a14df677cf7..acf6b0a286a 100644 --- a/src/uu/tail/src/chunks.rs +++ b/src/uu/tail/src/chunks.rs @@ -3,13 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) filehandle BUFSIZ - //! Iterating over a file by chunks, either starting at the end of the file with [`ReverseChunks`] //! or at the end of piped stdin with [`LinesChunk`] or [`BytesChunk`]. //! //! Use [`ReverseChunks::new`] to create a new iterator over chunks of bytes from the file. +// spell-checker:ignore (ToDO) filehandle BUFSIZ + use std::collections::VecDeque; use std::fs::File; use std::io::{BufRead, Read, Seek, SeekFrom, Write}; @@ -289,14 +289,14 @@ impl BytesChunkBuffer { // fill chunks with all bytes from reader and reuse already instantiated chunks if possible while chunk.fill(reader)?.is_some() { self.bytes += chunk.bytes as u64; - self.chunks.push_back(chunk); + self.chunks.push_back(chunk.clone()); let first = &self.chunks[0]; if self.bytes - first.bytes as u64 > self.num_print { chunk = self.chunks.pop_front().unwrap(); self.bytes -= chunk.bytes as u64; } else { - chunk = Box::new(BytesChunk::new()); + *chunk = BytesChunk::new(); } } @@ -563,14 +563,15 @@ impl LinesChunkBuffer { while chunk.fill(reader)?.is_some() { self.lines += chunk.lines as u64; - self.chunks.push_back(chunk); + self.chunks.push_back(chunk.clone()); let first = &self.chunks[0]; if self.lines - first.lines as u64 > self.num_print { chunk = self.chunks.pop_front().unwrap(); + self.lines -= chunk.lines as u64; } else { - chunk = Box::new(LinesChunk::new(self.delimiter)); + *chunk = LinesChunk::new(self.delimiter); } } diff --git a/src/uu/tsort/src/parser.rs b/src/uu/tsort/src/parser.rs index 5b2b427111a..0ee6778a238 100644 --- a/src/uu/tsort/src/parser.rs +++ b/src/uu/tsort/src/parser.rs @@ -28,7 +28,7 @@ where while pos < buf.len() { if pending.is_empty() { // Skip whitespace before the next token. - while buf.get(pos).is_some_and(|&b| is_delimiter(b)) { + while pos < buf.len() && is_delimiter(buf[pos]) { pos += 1; } diff --git a/src/uu/unexpand/locales/en-US.ftl b/src/uu/unexpand/locales/en-US.ftl index 881990c35e2..3a2a2092840 100644 --- a/src/uu/unexpand/locales/en-US.ftl +++ b/src/uu/unexpand/locales/en-US.ftl @@ -11,6 +11,6 @@ unexpand-help-no-utf8 = interpret input file as 8-bit ASCII rather than UTF-8 # Error messages unexpand-error-invalid-character = tab size contains invalid character(s): { $char } unexpand-error-tab-size-cannot-be-zero = tab size cannot be 0 -unexpand-error-tab-size-too-large = tab stop is too large +unexpand-error-tab-size-too-large = tab stop value is too large unexpand-error-tab-sizes-must-be-ascending = tab sizes must be ascending unexpand-error-is-directory = { $path }: Is a directory diff --git a/src/uu/unexpand/locales/fr-FR.ftl b/src/uu/unexpand/locales/fr-FR.ftl index 9a88abcafb3..44ad09a73d5 100644 --- a/src/uu/unexpand/locales/fr-FR.ftl +++ b/src/uu/unexpand/locales/fr-FR.ftl @@ -11,6 +11,6 @@ unexpand-help-no-utf8 = interpréter le fichier d'entrée comme ASCII 8-bit plut # Messages d'erreur unexpand-error-invalid-character = la taille de tabulation contient des caractères invalides : { $char } unexpand-error-tab-size-cannot-be-zero = la taille de tabulation ne peut pas être 0 -unexpand-error-tab-size-too-large = l'arrêt de tabulation est trop grand +unexpand-error-tab-size-too-large = la valeur d'arrêt de tabulation est trop grande unexpand-error-tab-sizes-must-be-ascending = les tailles de tabulation doivent être croissantes unexpand-error-is-directory = { $path } : Est un répertoire diff --git a/src/uu/uptime/src/main.rs b/src/uu/uptime/src/main.rs index 5823b728626..ec30c0d5cb1 100644 --- a/src/uu/uptime/src/main.rs +++ b/src/uu/uptime/src/main.rs @@ -3,4 +3,4 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -uucore::bin!(uu_uptime, no_flush); +uucore::bin!(uu_uptime); diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index a1973757f7a..471af4654cc 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -21,12 +21,9 @@ path = "src/users.rs" test = false doctest = false -[target.'cfg(any(target_vendor = "apple", target_os = "cygwin", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))'.dependencies] -uucore = { workspace = true, features = ["utmpx"] } - -[target.'cfg(any(target_vendor = "apple", target_os = "cygwin", target_os = "freebsd", target_os = "linux", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +[dependencies] clap = { workspace = true } -uucore = { workspace = true, features = [] } +uucore = { workspace = true, features = ["utmpx"] } fluent = { workspace = true } [target.'cfg(target_os = "openbsd")'.dependencies] @@ -38,4 +35,3 @@ workspace = true [[bin]] name = "users" path = "src/main.rs" -required-features = ["uucore/default"] diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index e71281548cf..5f8b217501d 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -5,15 +5,6 @@ // spell-checker:ignore (paths) wtmp -#![cfg(any( - target_vendor = "apple", - target_os = "cygwin", - target_os = "freebsd", - target_os = "linux", - target_os = "netbsd", - target_os = "openbsd" -))] - use std::ffi::OsString; use std::io::{Write, stdout}; use std::path::Path; diff --git a/src/uu/whoami/src/main.rs b/src/uu/whoami/src/main.rs index 7a6c9b9a1c1..0c9ee7f689c 100644 --- a/src/uu/whoami/src/main.rs +++ b/src/uu/whoami/src/main.rs @@ -3,4 +3,4 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -uucore::bin!(uu_whoami, no_flush); +uucore::bin!(uu_whoami); diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 77254042c7c..3f8841b6555 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -35,6 +35,7 @@ jiff = { workspace = true, optional = true, features = [ libc = { workspace = true, optional = true } os_display = { workspace = true } rustc-hash = { workspace = true } +rustix = { workspace = true, optional = true } # Not optional: os_display already pulls unicode-width into every uucore build, # so making it a direct dependency here is free and keeps char_width available # on all targets (a feature-gated optional dep failed to activate on wasm). @@ -99,9 +100,6 @@ bstr = { workspace = true, optional = true } [target.'cfg(any(target_vendor = "apple", target_os = "cygwin", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))'.dependencies] dns-lookup = { workspace = true, optional = true } -[target.'cfg(any(unix, windows, target_os = "wasi"))'.dependencies] -rustix = { workspace = true, optional = true } - [target.'cfg(unix)'.dependencies] # utmpx is unix-only, so its dependencies must not be pulled into other targets # (the uptime feature enables utmpx and now builds on windows too). @@ -156,7 +154,7 @@ encoding = ["data-encoding", "data-encoding-macro", "z85", "base64-simd"] entries = ["libc", "rustix/fs", "rustix/process"] extendedbigdecimal = ["bigdecimal", "num-traits"] fast-inc = [] -fs = ["dunce", "libc", "rustix/fs", "rustix/std", "windows-sys"] +fs = ["dunce", "libc", "rustix/fs", "windows-sys"] fsext = ["libc", "windows-sys", "bstr", "wide"] fsxattr = ["xattr", "itertools", "libc"] hardware = [] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 062af7b3f3c..e8b82567ad5 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -3,9 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (features) extendedbigdecimal logind - // features ~ feature-gated modules (core/bundler file) +// +// spell-checker:ignore (features) extendedbigdecimal logind #[cfg(feature = "backup-control")] pub mod backup_control; @@ -36,7 +36,7 @@ pub mod extendedbigdecimal; pub mod fast_inc; #[cfg(feature = "format")] pub mod format; -#[cfg(all(feature = "fs", any(unix, windows, target_os = "wasi")))] +#[cfg(feature = "fs")] pub mod fs; #[cfg(feature = "fsext")] pub mod fsext; diff --git a/src/uucore/src/lib/features/backup_control.rs b/src/uucore/src/lib/features/backup_control.rs index a47ff84f374..c508dd3598d 100644 --- a/src/uucore/src/lib/features/backup_control.rs +++ b/src/uucore/src/lib/features/backup_control.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore backupopt - //! Implement GNU-style backup functionality. //! //! This module implements the backup functionality as described in the [GNU @@ -82,6 +80,8 @@ //! } //! ``` +// spell-checker:ignore backupopt + use crate::{ display::Quotable, error::{UError, UResult}, diff --git a/src/uucore/src/lib/features/diagnostics.rs b/src/uucore/src/lib/features/diagnostics.rs index 791854a00bf..a891754c066 100644 --- a/src/uucore/src/lib/features/diagnostics.rs +++ b/src/uucore/src/lib/features/diagnostics.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore étage replacen - //! Render an error against the argument list it came from. //! //! Utilities whose arguments *are* the expression they evaluate — `test`, `expr` @@ -36,6 +34,8 @@ //! ───╯ //! ``` +// spell-checker:ignore étage replacen + use std::borrow::Cow; use std::env; use std::ffi::{OsStr, OsString}; diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 62c2ee822c0..92430fa0438 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -3,10 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore backport Ioctl absolutized linkat symlinkat renameat unlinkat openat urandom NOFOLLOW CLOEXEC RDONLY - //! Set of functions to manage regular files, special files, and links. +// spell-checker:ignore backport Ioctl absolutized linkat symlinkat renameat unlinkat openat urandom NOFOLLOW CLOEXEC RDONLY + #[cfg(all(unix, not(target_os = "haiku")))] pub use libc::{major, makedev, minor}; use std::collections::HashSet; @@ -126,25 +126,69 @@ impl FileInformation { } pub fn number_of_links(&self) -> u64 { - #[cfg(any(unix, target_os = "wasi"))] - { - #[cfg(any(target_os = "aix", target_os = "haiku"))] - return self.0.st_nlink.try_into().unwrap(); - #[cfg(not(any(target_os = "aix", target_os = "haiku")))] - #[allow(clippy::useless_conversion)] - return self.0.st_nlink.into(); - } + #[cfg(all( + unix, + not(target_vendor = "apple"), + not(target_os = "aix"), + not(target_os = "android"), + not(target_os = "freebsd"), + not(target_os = "haiku"), + not(target_os = "netbsd"), + not(target_os = "openbsd"), + not(target_os = "illumos"), + not(target_os = "solaris"), + not(target_os = "cygwin"), + not(target_arch = "aarch64"), + not(target_arch = "riscv64"), + not(target_arch = "loongarch64"), + not(target_arch = "sparc64"), + target_pointer_width = "64" + ))] + return self.0.st_nlink; + #[cfg(target_os = "wasi")] + return self.0.st_nlink; + #[cfg(all( + unix, + not(target_os = "haiku"), + any( + target_vendor = "apple", + target_os = "android", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris", + target_os = "cygwin", + target_arch = "aarch64", + target_arch = "riscv64", + target_arch = "loongarch64", + target_arch = "sparc64", + not(target_pointer_width = "64") + ) + ))] + return self.0.st_nlink.into(); + #[cfg(target_os = "freebsd")] + return self.0.st_nlink; + #[cfg(any(target_os = "aix", target_os = "haiku"))] + return self.0.st_nlink.try_into().unwrap(); #[cfg(windows)] return self.0.nNumberOfLinks as u64; } #[cfg(any(unix, target_os = "wasi"))] pub fn inode(&self) -> u64 { - #[cfg(target_os = "haiku")] - return self.0.st_ino.try_into().unwrap(); - #[cfg(not(target_os = "haiku"))] + #[cfg(all( + not(any(target_os = "haiku", target_os = "netbsd")), + target_pointer_width = "64" + ))] + return self.0.st_ino; + #[cfg(all( + not(target_os = "haiku"), + any(target_os = "netbsd", not(target_pointer_width = "64")) + ))] #[allow(clippy::useless_conversion)] return self.0.st_ino.into(); + #[cfg(target_os = "haiku")] + return self.0.st_ino.try_into().unwrap(); } } diff --git a/src/uucore/src/lib/features/fsext/mod.rs b/src/uucore/src/lib/features/fsext/mod.rs index 77136cf62a3..5286089ad66 100644 --- a/src/uucore/src/lib/features/fsext/mod.rs +++ b/src/uucore/src/lib/features/fsext/mod.rs @@ -3,10 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore DATETIME getmntinfo subsecond (fs) cifs smbfs - //! Set of functions to manage file systems +// spell-checker:ignore DATETIME getmntinfo subsecond (fs) cifs smbfs + #[cfg(windows)] mod windows; diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index b180cf9a654..7ecfd06b1b9 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -6,7 +6,9 @@ // spell-checker:ignore getxattr posix_acl_default posix_acl_access ENOTSUP EOPNOTSUPP renamer //! Set of functions to manage xattr on files and dirs - +use crate::display::Quotable; +use crate::error::strip_errno; +use crate::show_error; use itertools::Itertools; use rustc_hash::FxHashMap; use std::ffi::{OsStr, OsString}; @@ -17,7 +19,7 @@ use std::path::Path; /// True if the error is `ENOTSUP` / `EOPNOTSUPP` (same errno on Linux, /// distinct on the BSDs). #[cfg(unix)] -fn is_xattr_unsupported(err: &std::io::Error) -> bool { +pub fn is_xattr_unsupported(err: &std::io::Error) -> bool { matches!( err.raw_os_error(), Some(e) if e == libc::ENOTSUP || e == libc::EOPNOTSUPP @@ -25,20 +27,55 @@ fn is_xattr_unsupported(err: &std::io::Error) -> bool { } #[cfg(not(unix))] -fn is_xattr_unsupported(_err: &std::io::Error) -> bool { +pub fn is_xattr_unsupported(_err: &std::io::Error) -> bool { false } +/// Report a per-attribute failure on stderr and record it so the copy loop +/// can keep working on the remaining attributes and still fail afterwards. +/// +/// `ENOTSUP` / `EOPNOTSUPP` mean the filesystem simply has no xattr support +/// and are recorded but not reported: best-effort callers map them to `Ok` +/// through the `*_ignore_unsupported` wrappers and must stay quiet. +fn record_xattr_failure( + attr_name: &OsStr, + reading: bool, + err: std::io::Error, + pending_error: &mut Option, +) { + if !is_xattr_unsupported(&err) { + let action = if reading { + "cannot read attribute" + } else { + "setting attribute" + }; + show_error!("{action} {}: {}", attr_name.quote(), strip_errno(&err)); + } + if pending_error.is_none() { + *pending_error = Some(err); + } +} + /// Copies extended attributes (xattrs) from one path to another. -/// All errors propagate, including `ENOTSUP` / `EOPNOTSUPP`; for +/// +/// A failed attribute is reported on stderr and does not stop the other +/// attributes from being copied; the first such failure is propagated at +/// the end. `ENOTSUP` / `EOPNOTSUPP` are recorded but not reported; for /// best-effort callers see [`copy_xattrs_ignore_unsupported`]. pub fn copy_xattrs>(source: P, dest: P) -> std::io::Result<()> { + let mut pending_error = None; for attr_name in xattr::list(&source)? { - if let Some(value) = xattr::get(&source, &attr_name)? { - xattr::set(&dest, &attr_name, &value)?; + match xattr::get(&source, &attr_name) { + Ok(Some(value)) => { + if let Err(err) = xattr::set(&dest, &attr_name, &value) { + record_xattr_failure(&attr_name, false, err, &mut pending_error); + } + } + Ok(None) => {} + Err(err) => record_xattr_failure(&attr_name, true, err, &mut pending_error), } } - Ok(()) + pending_error.map_or(Ok(()), Err) } /// Like [`copy_xattrs`], but maps `ENOTSUP` / `EOPNOTSUPP` to `Ok(())` @@ -53,15 +90,25 @@ pub fn copy_xattrs_ignore_unsupported>(source: P, dest: P) -> std /// Copies xattrs between two open file descriptors. Pins both inodes so /// list/get/set calls cannot be redirected by a concurrent renamer, unlike /// the path-based [`copy_xattrs`]. +/// +/// Failures are handled like in [`copy_xattrs`]: each one is reported and +/// the remaining attributes are still copied. #[cfg(unix)] pub fn copy_xattrs_fd(source: &std::fs::File, dest: &std::fs::File) -> std::io::Result<()> { use xattr::FileExt; + let mut pending_error = None; for attr_name in source.list_xattr()? { - if let Some(value) = source.get_xattr(&attr_name)? { - dest.set_xattr(&attr_name, &value)?; + match source.get_xattr(&attr_name) { + Ok(Some(value)) => { + if let Err(err) = dest.set_xattr(&attr_name, &value) { + record_xattr_failure(&attr_name, false, err, &mut pending_error); + } + } + Ok(None) => {} + Err(err) => record_xattr_failure(&attr_name, true, err, &mut pending_error), } } - Ok(()) + pending_error.map_or(Ok(()), Err) } /// Like [`copy_xattrs_fd`], but maps `ENOTSUP` / `EOPNOTSUPP` to `Ok(())`. @@ -77,16 +124,27 @@ pub fn copy_xattrs_fd_ignore_unsupported( } /// Like `copy_xattrs`, but skips the security.selinux attribute. +/// +/// Failures are handled like in [`copy_xattrs`]: each one is reported and +/// the remaining attributes are still copied. #[cfg(unix)] pub fn copy_xattrs_skip_selinux>(source: P, dest: P) -> std::io::Result<()> { + let mut pending_error = None; for attr_name in xattr::list(&source)? { - if attr_name.as_bytes() != b"security.selinux" - && let Some(value) = xattr::get(&source, &attr_name)? - { - xattr::set(&dest, &attr_name, &value)?; + if attr_name.as_bytes() == b"security.selinux" { + continue; + } + match xattr::get(&source, &attr_name) { + Ok(Some(value)) => { + if let Err(err) = xattr::set(&dest, &attr_name, &value) { + record_xattr_failure(&attr_name, false, err, &mut pending_error); + } + } + Ok(None) => {} + Err(err) => record_xattr_failure(&attr_name, true, err, &mut pending_error), } } - Ok(()) + pending_error.map_or(Ok(()), Err) } /// Copies only the POSIX ACL xattrs (`system.posix_acl_access` and @@ -156,6 +214,9 @@ pub fn retrieve_xattrs_fd(source: &std::fs::File) -> std::io::Result>( dest: P, xattrs: FxHashMap>, ) -> std::io::Result<()> { + let mut pending_error = None; for (attr, value) in xattrs { - xattr::set(&dest, &attr, &value)?; + if let Err(err) = xattr::set(&dest, &attr, &value) { + record_xattr_failure(&attr, false, err, &mut pending_error); + } } - Ok(()) + pending_error.map_or(Ok(()), Err) } /// Applies extended attributes (xattrs) to a given file using a file descriptor. /// -/// This version avoids TOCTOU races by operating on an open file descriptor -/// rather than a path, ensuring all operations target the same inode. +/// Failures are handled like in [`copy_xattrs`]: each one is reported and +/// the remaining attributes are still applied. /// /// # Arguments /// @@ -193,10 +257,13 @@ pub fn apply_xattrs_fd( xattrs: FxHashMap>, ) -> std::io::Result<()> { use xattr::FileExt; + let mut pending_error = None; for (attr, value) in xattrs { - dest.set_xattr(&attr, &value)?; + if let Err(err) = dest.set_xattr(&attr, &value) { + record_xattr_failure(&attr, false, err, &mut pending_error); + } } - Ok(()) + pending_error.map_or(Ok(()), Err) } /// Like [`apply_xattrs_fd`], but maps `ENOTSUP` / `EOPNOTSUPP` to `Ok(())`. @@ -370,6 +437,81 @@ mod tests { assert_eq!(copied, test_value); } + #[test] + #[cfg(target_os = "linux")] + fn test_copy_xattrs_continues_after_failure() { + use std::path::PathBuf; + use std::process::Command; + + // tmpfs accepts large user-xattr values while most disk filesystems + // cap them near the block size. Put the source on /dev/shm and the + // destination on the build filesystem so the first attribute fails to + // copy while the source holds it fine; skip when this machine cannot + // produce that layout. + let pid = std::process::id(); + let source_dir = PathBuf::from(format!("/dev/shm/xattr_copy_fail_{pid}")); + let dest_dir = std::env::temp_dir().join(format!("xattr_copy_fail_{pid}")); + if std::fs::create_dir(&source_dir).is_err() || std::fs::create_dir(&dest_dir).is_err() { + return; // skip: no usable /dev/shm or temp dir + } + + let mut usable_size = None; + for size in [9_100, 40_000] { + let value = "y".repeat(size); + let source_probe = source_dir.join(format!("probe_{size}")); + let dest_probe = dest_dir.join(format!("probe_{size}")); + std::fs::write(&source_probe, "x").ok(); + std::fs::write(&dest_probe, "x").ok(); + let src_accepts = Command::new("setfattr") + .args(["-n", "user.huge", "-v", &value]) + .arg(&source_probe) + .status() + .is_ok_and(|s| s.success()); + let dest_rejects = !Command::new("setfattr") + .args(["-n", "user.huge", "-v", &value]) + .arg(&dest_probe) + .status() + .is_ok_and(|s| s.success()); + std::fs::remove_file(&source_probe).ok(); + std::fs::remove_file(&dest_probe).ok(); + if src_accepts && dest_rejects { + usable_size = Some(size); + break; + } + } + let Some(size) = usable_size else { + std::fs::remove_dir_all(&source_dir).ok(); + std::fs::remove_dir_all(&dest_dir).ok(); + return; // skip: this filesystem combination cannot fail the copy + }; + + // Set small attributes around the failing big attribute so that + // regardless of filesystem listing order (alphabetical, insertion, + // or reverse-insertion), at least one surviving attribute is + // processed after the failing one. + let source = source_dir.join("source"); + let dest = dest_dir.join("dest"); + std::fs::write(&source, "data").unwrap(); + std::fs::write(&dest, "data").unwrap(); + let big_value = "y".repeat(size); + xattr::set(&source, "user.a_small", b"12345678").unwrap(); + xattr::set(&source, "user.m_big", big_value.as_bytes()).unwrap(); + xattr::set(&source, "user.z_small", b"87654321").unwrap(); + + let result = copy_xattrs(&source, &dest); + assert!(result.is_err(), "the failed attribute must fail the copy"); + + let copied_a = xattr::get(&dest, "user.a_small").unwrap(); + assert_eq!(copied_a.as_deref(), Some(b"12345678".as_slice())); + let copied_z = xattr::get(&dest, "user.z_small").unwrap(); + assert_eq!(copied_z.as_deref(), Some(b"87654321".as_slice())); + let copied_big = xattr::get(&dest, "user.m_big").unwrap(); + assert_eq!(copied_big, None); + + std::fs::remove_dir_all(&source_dir).ok(); + std::fs::remove_dir_all(&dest_dir).ok(); + } + #[test] fn test_apply_and_retrieve_xattrs() { let temp_dir = tempdir().unwrap(); diff --git a/src/uucore/src/lib/features/i18n/mod.rs b/src/uucore/src/lib/features/i18n/mod.rs index a31e303c1c6..cd05e23b88a 100644 --- a/src/uucore/src/lib/features/i18n/mod.rs +++ b/src/uucore/src/lib/features/i18n/mod.rs @@ -76,11 +76,12 @@ pub fn get_locale_from_os() -> (Locale, UEncoding) { WideCharToMultiByte, }; - /// TODO(MSRV>=1.93): remove in favor of `slice::assume_init_ref` + /// assume_init_ref is only stable starting Rust 1.93. + /// We cannot use it in the current MSRV of 1.88. /// /// # Safety /// - /// Same as the official [`slice::assume_init_ref`](https://doc.rust-lang.org/1.93.0/std/primitive.slice.html#method.assume_init_ref). + /// Same as the official `assume_init_ref`. #[allow(clippy::ref_as_ptr)] unsafe fn assume_init_ref(s: &[MaybeUninit]) -> &[T] { unsafe { &*(s as *const [MaybeUninit] as *const [T]) } diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 77aef656d12..8dfeccb4c94 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -3,10 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars) fperm srwx - //! Set of functions to parse modes +// spell-checker:ignore (vars) fperm srwx + use std::fmt::{self, Display}; use std::ops::Range; diff --git a/src/uucore/src/lib/features/parser/num_parser.rs b/src/uucore/src/lib/features/parser/num_parser.rs index 2b5d34b0d4c..991b870ca03 100644 --- a/src/uucore/src/lib/features/parser/num_parser.rs +++ b/src/uucore/src/lib/features/parser/num_parser.rs @@ -3,10 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore powf copysign prec ilog inity infinit infs bigdecimal extendedbigdecimal biguint underflowed muls - //! Utilities for parsing numbers in various formats +// spell-checker:ignore powf copysign prec ilog inity infinit infs bigdecimal extendedbigdecimal biguint underflowed muls + use bigdecimal::{ BigDecimal, num_bigint::{BigInt, BigUint, Sign}, diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 89dc4b5bffc..b0c4a6fd26e 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -3,10 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (jargon) TOCTOU fchownat fchown - //! Common functions to manage permissions +// spell-checker:ignore (jargon) TOCTOU fchownat fchown + use crate::display::Quotable; use crate::error::{UResult, USimpleError, strip_errno}; pub use crate::features::entries; diff --git a/src/uucore/src/lib/features/process/unix.rs b/src/uucore/src/lib/features/process/unix.rs index 9bfa86ba747..4bfc4155340 100644 --- a/src/uucore/src/lib/features/process/unix.rs +++ b/src/uucore/src/lib/features/process/unix.rs @@ -182,15 +182,16 @@ mod timer { pub(super) fn arm(&mut self, timeout: Duration) -> Result<(), io::Error> { let timeout = timeout.min(MAX_KTIME_T).max(Duration::from_micros(1)); - // `timespec` has private padding members on time64 targets, so its - // fields cannot be listed in a struct literal; start from the - // zeroed default and fill in the ones we care about. - let mut time = libc::itimerspec { - it_interval: libc::timespec::default(), - it_value: libc::timespec::default(), + let time = libc::itimerspec { + it_interval: libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }, + it_value: libc::timespec { + tv_sec: timeout.as_secs() as _, + tv_nsec: timeout.subsec_nanos() as _, + }, }; - time.it_value.tv_sec = timeout.as_secs() as _; - time.it_value.tv_nsec = timeout.subsec_nanos() as _; // SAFETY: All values are properly initialized. if unsafe { libc::timer_settime(self.0, 0, &raw const time, null_mut()) } == -1 { diff --git a/src/uucore/src/lib/features/ranges.rs b/src/uucore/src/lib/features/ranges.rs index 5688e93d8a2..b2e5d0cf133 100644 --- a/src/uucore/src/lib/features/ranges.rs +++ b/src/uucore/src/lib/features/ranges.rs @@ -245,15 +245,17 @@ impl Range { /// /// Is guaranteed to return only disjoint ranges in a sorted order. pub fn merge(mut ranges: Vec) -> Vec { - ranges.sort_unstable_by_key(|r| r.low); - ranges.dedup_by(|a, b| { - if a.low <= b.high { - b.high = max(b.high, a.high); - true - } else { - false + ranges.sort(); + + // merge overlapping ranges + for i in 0..ranges.len() { + let j = i + 1; + + while j < ranges.len() && ranges[j].low <= ranges[i].high { + let j_high = ranges.remove(j).high; + ranges[i].high = max(ranges[i].high, j_high); } - }); + } ranges } } diff --git a/src/uucore/src/lib/features/safe_traversal.rs b/src/uucore/src/lib/features/safe_traversal.rs index 39fcb0e8023..4f573ad0359 100644 --- a/src/uucore/src/lib/features/safe_traversal.rs +++ b/src/uucore/src/lib/features/safe_traversal.rs @@ -3,15 +3,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore CLOEXEC RDONLY TOCTOU closedir dirp fdopendir fstatat openat REMOVEDIR unlinkat smallfile -// spell-checker:ignore RAII dirfd fchownat fchown FchmodatFlags fchmodat fchmod mkdirat CREAT WRONLY ELOOP ENOTDIR EXCL EEXIST -// spell-checker:ignore atimensec mtimensec ctimensec opath chmods fakeroot fakechroot -// spell-checker:ignore LARGEFILE - +// // Safe directory traversal using openat() and related syscalls // This module provides TOCTOU-safe filesystem operations for recursive traversal // // Available on Unix +// +// spell-checker:ignore CLOEXEC RDONLY TOCTOU closedir dirp fdopendir fstatat openat REMOVEDIR unlinkat smallfile +// spell-checker:ignore RAII dirfd fchownat fchown FchmodatFlags fchmodat fchmod mkdirat CREAT WRONLY ELOOP ENOTDIR EXCL EEXIST +// spell-checker:ignore atimensec mtimensec ctimensec opath chmods fakeroot fakechroot +// spell-checker:ignore LARGEFILE #[cfg(test)] use std::os::unix::ffi::OsStringExt; diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index fcf8fcac6af..53781b7f92a 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -3,11 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind - //! library ~ (core/bundler file) - // #![deny(missing_docs)] //TODO: enable this +// +// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind // * feature-gated external crates (re-shared as public internal modules) #[cfg(feature = "libc")] @@ -27,7 +26,7 @@ pub use uucore_procs::*; pub use crate::mods::clap_localization; pub use crate::mods::display; pub use crate::mods::error; -#[cfg(all(feature = "fs", any(unix, windows, target_os = "wasi")))] +#[cfg(feature = "fs")] pub use crate::mods::io; pub use crate::mods::line_ending; pub use crate::mods::locale; @@ -56,7 +55,7 @@ pub use crate::features::extendedbigdecimal; pub use crate::features::fast_inc; #[cfg(feature = "format")] pub use crate::features::format; -#[cfg(all(feature = "fs", any(unix, windows, target_os = "wasi")))] +#[cfg(feature = "fs")] pub use crate::features::fs; #[cfg(feature = "hardware")] pub use crate::features::hardware; diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 9528549fe8b..857e843546b 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore sourcepath targetpath rustdoc - //! Macros for the uucore utilities. //! //! This module bundles all macros used across the uucore utilities. These @@ -33,6 +31,8 @@ //! - From custom messages: [`crate::show_error!`] //! - Print warnings: [`crate::show_warning!`] +// spell-checker:ignore sourcepath targetpath rustdoc + use std::sync::atomic::AtomicBool; // This file is part of the uutils coreutils package. diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 2d5c94a4be6..63f29912fd8 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -8,7 +8,7 @@ pub mod clap_localization; pub mod display; pub mod error; -#[cfg(all(feature = "fs", any(unix, windows, target_os = "wasi")))] +#[cfg(feature = "fs")] pub mod io; pub mod line_ending; pub mod locale; diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 32cfa0e8a46..c894774078e 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore uioerror rustdoc - //! All utils return exit with an exit code. Usually, the following scheme is used: //! * `0`: succeeded //! * `1`: minor problems @@ -55,6 +53,8 @@ //! * Using [`ExitCode`] is not recommended but can be useful for converting utils to use //! [`UResult`]. +// spell-checker:ignore uioerror rustdoc + use std::{ cell::Cell, error::Error, diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index 3c09978ab82..0b49087bc8e 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -228,24 +228,16 @@ fn find_uucore_locales_dir(utility_locales_dir: &Path) -> Option { .canonicalize() .unwrap_or_else(|_| utility_locales_dir.to_path_buf()); - // In the source tree, walk up: locales -> printenv -> uu -> src - let in_source_tree = normalized_dir - .parent() // printenv - .and_then(Path::parent) // uu - .and_then(Path::parent) // src - .map(|src| src.join("uucore").join("locales")); - - // Next to an installed binary, the directory sits beside the one of the - // utility: /printenv -> /uucore - let installed = normalized_dir - .parent() - .map(|locales| locales.join("uucore")); - - // Only return a directory that actually exists - [in_source_tree, installed] - .into_iter() - .flatten() - .find(|dir| dir.exists()) + // Walk up: locales -> printenv -> uu -> src + let uucore_locales = normalized_dir + .parent()? // printenv + .parent()? // uu + .parent()? // src + .join("uucore") + .join("locales"); + + // Only return if the directory actually exists + uucore_locales.exists().then_some(uucore_locales) } /// Create a bundle that combines common and utility-specific strings @@ -1143,28 +1135,6 @@ invalid-syntax = This is { $missing } } - /// The common strings also have to be found next to an installed binary, - /// where there is no source tree to walk up and the uucore directory sits - /// beside the one of the utility. - #[test] - fn test_find_uucore_locales_dir_installed_layout() { - // /share/locales/fake_util/ <- locales directory of the utility - // /share/locales/uucore/ <- common strings - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let locales = temp_dir.path().join("share").join("locales"); - let util_dir = locales.join("fake_util"); - let uucore_dir = locales.join("uucore"); - - fs::create_dir_all(&util_dir).expect("Failed to create fake util locales dir"); - assert_eq!(find_uucore_locales_dir(&util_dir), None); - - fs::create_dir_all(&uucore_dir).expect("Failed to create fake uucore locales dir"); - assert_eq!( - find_uucore_locales_dir(&util_dir), - Some(uucore_dir.canonicalize().unwrap()) - ); - } - #[test] fn test_localizer_format_primary_bundle() { let temp_dir = create_test_locales_dir(); diff --git a/tests/by-util/test_b2sum.rs b/tests/by-util/test_b2sum.rs index 216284c98e8..08b13fabf90 100644 --- a/tests/by-util/test_b2sum.rs +++ b/tests/by-util/test_b2sum.rs @@ -3,14 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore checkfile, testf, ntestf - use rstest::rstest; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; - +// spell-checker:ignore checkfile, testf, ntestf macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index 004539b752e..9dfc5b3000b 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -219,18 +219,6 @@ fn test_wrap_bad_arg() { } } -#[test] -fn test_wrap_negative_arg() { - // GNU treats the token after -w as the wrap size even if it starts with '-'. - for arg in ["-5", "-d"] { - new_ucmd!() - .arg("-w") - .arg(arg) - .fails() - .stderr_only(format!("base64: invalid wrap size: '{arg}'\n")); - } -} - #[test] fn test_base64_extra_operand() { // Expect a failure when multiple files are specified. diff --git a/tests/by-util/test_basename.rs b/tests/by-util/test_basename.rs index 59f1f2036a6..6e023c2f984 100644 --- a/tests/by-util/test_basename.rs +++ b/tests/by-util/test_basename.rs @@ -184,6 +184,12 @@ fn test_invalid_utf8_args() { .stdout_is_bytes(b"some-\xc0-file\n"); } +#[test] +fn test_root() { + let expected = if cfg!(windows) { "\\\n" } else { "/\n" }; + new_ucmd!().arg("/").succeeds().stdout_is(expected); +} + #[test] fn test_double_slash() { // TODO The GNU tests seem to suggest that some systems treat "//" @@ -201,6 +207,12 @@ fn test_double_slash() { .stdout_is(expected); } +#[test] +fn test_triple_slash() { + let expected = if cfg!(windows) { "\\\n" } else { "/\n" }; + new_ucmd!().arg("///").succeeds().stdout_is(expected); +} + #[test] fn test_trailing_dot() { new_ucmd!().arg("/.").succeeds().stdout_is(".\n"); diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 54188b30dad..dd312e364c5 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -5,7 +5,6 @@ // spell-checker:ignore (flags) reflink (fs) tmpfs (linux) filefrag rlimit Rlim Nofile clob btrfs neve ROOTDIR USERDIR outfile subvolume uufs xattrs ELOOP // spell-checker:ignore bdfl hlsl IRWXO IRWXG nconfined matchpathcon libselinux-devel prwx doesnotexist reftests subdirs mksocket srwx dstlink mcstransd - #[cfg(unix)] use rstest::rstest; use uucore::display::Quotable; @@ -1056,80 +1055,6 @@ fn test_cp_umask_stripping_owner_write_bit_reflink_never() { } } -// Regression for #14549: `cp -r` (without preserve) must apply the umask to -// directories it creates, matching GNU, instead of copying the source's mode. -#[test] -#[cfg(unix)] -fn test_cp_recursive_dir_applies_umask() { - let (at, mut ucmd) = at_and_ucmd!(); - at.mkdir("src"); - at.mkdir("src/dir"); - at.set_mode("src/dir", 0o777); - - ucmd.umask(0o077).args(&["-r", "src", "d"]).succeeds(); - - // 0o777 & ~0o077 = 0o700, not the source's raw 0o777. - assert_eq!(at.metadata("d/dir").permissions().mode() & 0o777, 0o700); -} - -// The umask alone never covers setuid/setgid, so a non-preserving `cp -r` -// must clear them on the directories it creates. The sticky bit survives. -#[test] -#[cfg(unix)] -#[cfg_attr( - wasi_runner, - ignore = "WASI: directory modes/umask are not faithfully reproduced" -)] -fn test_cp_recursive_dir_drops_setuid_setgid() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; - - at.mkdir("tree"); - for (name, mode) in [ - ("tree/setgid", 0o2731u32), - ("tree/setuid", 0o4713), - ("tree/sticky", 0o1735), - ] { - at.mkdir(name); - at.set_mode(name, mode); - } - - scene - .ucmd() - .umask(0o026) - .args(&["-r", "tree", "plain"]) - .succeeds(); - - assert_eq!( - at.metadata("plain/setgid").permissions().mode() & 0o7777, - 0o711 - ); - assert_eq!( - at.metadata("plain/setuid").permissions().mode() & 0o7777, - 0o711 - ); - assert_eq!( - at.metadata("plain/sticky").permissions().mode() & 0o7777, - 0o1711 - ); - - // An explicit preserve keeps the mode as-is, umask and special bits alike. - scene - .ucmd() - .umask(0o026) - .args(&["-r", "--preserve=mode", "tree", "kept"]) - .succeeds(); - - assert_eq!( - at.metadata("kept/setgid").permissions().mode() & 0o7777, - 0o2731 - ); - assert_eq!( - at.metadata("kept/setuid").permissions().mode() & 0o7777, - 0o4713 - ); -} - // When --reflink=always fails, GNU cp removes a destination it created // itself but keeps a pre-existing (truncated) one. Only observable on // filesystems without clone support; when the clone succeeds there is @@ -5533,13 +5458,12 @@ fn test_acl_preserve() { fn test_cp_debug_reflink_never_with_hole() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let page_size = rustix::param::page_size(); at.write("a", "hello"); let f = std::fs::OpenOptions::new() .write(true) .open(at.plus("a")) .unwrap(); - f.set_len((page_size as u64) * 4).unwrap(); + f.set_len(10000).unwrap(); ts.ucmd() .arg("--debug") @@ -5587,12 +5511,11 @@ fn test_cp_debug_default_with_hole() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; at.touch("a"); - let page_size = rustix::param::page_size(); let f = std::fs::OpenOptions::new() .write(true) .open(at.plus("a")) .unwrap(); - f.set_len((page_size as u64) * 4).unwrap(); + f.set_len(10000).unwrap(); at.append_bytes("a", "hello".as_bytes()); @@ -5686,13 +5609,12 @@ fn test_cp_debug_default_empty_file_with_hole() { fn test_cp_debug_reflink_never_sparse_always_with_hole() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let page_size = rustix::param::page_size(); at.write("a", "hello"); let f = std::fs::OpenOptions::new() .write(true) .open(at.plus("a")) .unwrap(); - f.set_len((page_size as u64) * 4).unwrap(); + f.set_len(10000).unwrap(); ts.ucmd() .arg("--debug") @@ -5937,13 +5859,12 @@ fn test_cp_debug_reflink_never_file_with_hole() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; at.touch("a"); - let page_size = rustix::param::page_size(); let f = std::fs::OpenOptions::new() .write(true) .open(at.plus("a")) .unwrap(); - f.set_len((page_size as u64) * 4).unwrap(); - at.append_bytes("a", b"hello"); + f.set_len(10000).unwrap(); + at.append_bytes("a", "hello".as_bytes()); ts.ucmd() .arg("--debug") @@ -6031,13 +5952,12 @@ fn test_cp_debug_sparse_never_empty_file_with_hole() { fn test_cp_debug_sparse_never_file_with_hole() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let page_size = rustix::param::page_size(); at.touch("a"); let f = std::fs::OpenOptions::new() .write(true) .open(at.plus("a")) .unwrap(); - f.set_len((page_size as u64) * 4).unwrap(); + f.set_len(10000).unwrap(); at.append_bytes("a", "hello".as_bytes()); ts.ucmd() @@ -9524,7 +9444,7 @@ fn test_cp_xattr_failure_keeps_dest_contents() { .arg(&source) .arg(&out) .fails() - .stderr_contains("setting attributes"); + .stderr_contains("setting attribute 'user.huge'"); assert_eq!(std_fs::read_to_string(&out).unwrap(), "kept content"); // A read-only source propagates its mode to the destination; the failure @@ -9537,7 +9457,7 @@ fn test_cp_xattr_failure_keeps_dest_contents() { .arg(&source) .arg(&out_ro) .fails() - .stderr_contains("setting attributes"); + .stderr_contains("setting attribute 'user.huge'"); assert_eq!(std_fs::read_to_string(&out_ro).unwrap(), "kept content"); assert_eq!( std_fs::metadata(&out_ro).unwrap().mode() & 0o777, diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 51ce9ed5797..7f9e2c4eceb 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -11,7 +11,7 @@ use std::cmp::Ordering; use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan}; use regex::Regex; -#[cfg(unix)] +#[cfg(all(unix, not(target_vendor = "apple")))] use rustix::process::geteuid; use uutests::util::TestScenario; #[cfg(unix)] @@ -511,7 +511,7 @@ fn test_date_format_literal() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, not(target_vendor = "apple")))] fn test_date_set_valid() { if geteuid().is_root() { new_ucmd!() @@ -523,7 +523,7 @@ fn test_date_set_valid() { } #[test] -#[cfg(any(windows, unix))] +#[cfg(any(windows, all(unix, not(target_vendor = "apple"))))] fn test_date_set_invalid() { let result = new_ucmd!().arg("--set").arg("123abcd").fails(); result.no_stdout(); @@ -531,25 +531,7 @@ fn test_date_set_invalid() { } #[test] -fn test_date_error_echoes_input_verbatim() { - // Error messages echo what the user typed: numeric-looking input must - // not be reformatted as a Fluent number (#14669, #14670). - for input in ["1e9", "+1e-2", "+9.e-0", "9.", "-0"] { - new_ucmd!() - .arg("-d") - .arg(input) - .fails() - .stderr_is(format!("date: invalid date '{input}'\n")); - } - // Same for the missing '+' message, which echoes the argument too. - new_ucmd!() - .args(&["--date", "1996-01-31", "1e9"]) - .fails_with_code(1) - .stderr_contains("the argument 1e9 lacks a leading '+'"); -} - -#[test] -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(all(unix, not(any(target_vendor = "apple", target_os = "android"))))] fn test_date_set_permissions_error() { if !(geteuid().is_root() || uucore::os::is_wsl_1()) { let result = new_ucmd!() @@ -562,7 +544,7 @@ fn test_date_set_permissions_error() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(all(unix, not(any(target_vendor = "apple", target_os = "android"))))] fn test_date_set_hyphen_prefixed_values() { // test -s flag accepts hyphen-prefixed values like "-3 days" if !(geteuid().is_root() || uucore::os::is_wsl_1()) { @@ -582,7 +564,22 @@ fn test_date_set_hyphen_prefixed_values() { } #[test] -#[cfg(unix)] +#[cfg(target_vendor = "apple")] +fn test_date_set_mac_unavailable() { + let result = new_ucmd!() + .arg("--set") + .arg("2020-03-11 21:45:00+08:00") + .fails(); + result.no_stdout(); + assert!( + result + .stderr_str() + .starts_with("date: setting the date is not supported by macOS") + ); +} + +#[test] +#[cfg(all(unix, not(target_vendor = "apple")))] fn test_date_set_valid_2() { if geteuid().is_root() { new_ucmd!() @@ -594,12 +591,13 @@ fn test_date_set_valid_2() { } #[test] -fn test_date_for_non_existing_file() { - new_ucmd!() - .arg("--file") - .arg("non_existing_file") - .fails() - .stderr_only("date: non_existing_file: No such file or directory\n"); +fn test_date_for_invalid_file() { + let result = new_ucmd!().arg("--file").arg("invalid_file").fails(); + result.no_stdout(); + assert_eq!( + result.stderr_str().trim(), + "date: invalid_file: No such file or directory", + ); } #[test] @@ -618,42 +616,30 @@ fn test_date_for_no_permission_file() { .unwrap(); file.set_permissions(std::fs::Permissions::from_mode(0o222)) .unwrap(); - - ucmd.arg("--file") - .arg(FILE) - .fails() - .stderr_only(format!("date: {FILE}: Permission denied\n")); + let result = ucmd.arg("--file").arg(FILE).fails(); + result.no_stdout(); + assert_eq!( + result.stderr_str().trim(), + format!("date: {FILE}: Permission denied") + ); } #[test] fn test_date_for_dir_as_file() { - new_ucmd!() - .arg("--file") - .arg("/") - .fails_with_code(1) - .stderr_only("date: expected file, got directory '/'\n"); + let result = new_ucmd!().arg("--file").arg("/").fails_with_code(1); + result.no_stdout(); + assert_eq!( + result.stderr_str().trim(), + "date: expected file, got directory '/'", + ); } #[test] -fn test_date_for_empty_file() { +fn test_date_for_file() { let (at, mut ucmd) = at_and_ucmd!(); let file = "test_date_for_file"; at.touch(file); - ucmd.arg("--file").arg(file).succeeds().no_output(); -} - -#[test] -#[cfg(target_os = "linux")] -#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] -fn test_date_for_file_with_non_utf8_path() { - use std::os::unix::ffi::OsStrExt; - - let (at, mut ucmd) = at_and_ucmd!(); - - let file = std::ffi::OsStr::from_bytes(b"file_\xFF\xFE.txt"); - std::fs::File::create(at.plus(file)).unwrap(); - - ucmd.arg("--file").arg(file).succeeds().no_output(); + ucmd.arg("--file").arg(file).succeeds(); } #[test] @@ -714,27 +700,6 @@ fn test_date_for_file_mtime() { .stdout_only("1234\n"); } -#[test] -#[cfg(target_os = "linux")] -#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] -fn test_date_reference_is_non_utf8_path() { - use std::os::unix::ffi::OsStrExt; - use std::time::{Duration, UNIX_EPOCH}; - - let (at, mut ucmd) = at_and_ucmd!(); - - let reference_file = std::ffi::OsStr::from_bytes(b"reference_\xFF\xFE.txt"); - let f = std::fs::File::create(at.plus(reference_file)).unwrap(); - let modification_date = UNIX_EPOCH.checked_add(Duration::from_secs(1234)).unwrap(); - f.set_modified(modification_date).unwrap(); - - ucmd.arg("--reference") - .arg(reference_file) - .arg("+%s") - .succeeds() - .stdout_only("1234\n"); -} - #[test] fn test_date_multiple_references() { use std::time::{Duration, UNIX_EPOCH}; @@ -763,7 +728,7 @@ fn test_date_multiple_references() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, not(target_vendor = "apple")))] fn test_date_set_valid_3() { if geteuid().is_root() { new_ucmd!() @@ -775,7 +740,7 @@ fn test_date_set_valid_3() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, not(target_vendor = "apple")))] fn test_date_set_valid_4() { if geteuid().is_root() { new_ucmd!() diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index 9cb2a118d9c..fed1729f755 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -581,18 +581,6 @@ fn test_invalid_substr() { .stdout_only("\n"); } -#[test] -#[cfg_attr( - wasi_runner, - ignore = "WASI: usize is 32-bit, the host usize::MAX does not parse" -)] -fn test_substr_large_length_capacity_overflow() { - new_ucmd!() - .args(&["substr", "abc", "1", &usize::MAX.to_string()]) - .succeeds() - .stdout_only("abc\n"); -} - #[test] fn test_escape() { new_ucmd!().args(&["+", "1"]).succeeds().stdout_only("1\n"); diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 10290f9f6ec..53de6bd9b38 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore (methods) hexdigest funcs nprimes cmdline - #![allow( clippy::similar_names, clippy::cast_possible_truncation, diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index ecff20dd488..c19125b1a54 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore plass samp FFFD - #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStringExt; use uutests::new_ucmd; @@ -506,32 +505,3 @@ fn test_fmt_width_multiplication_overflow() { .fails_with_code(1) .stderr_is("fmt: invalid width: '267672676527678256'\n"); } - -#[test] -fn test_fmt_goal_only_defaults_width_to_goal_plus_ten() { - // GNU defaults the width to goal + 10 when only --goal is given, so `-g G` - // has to lay a paragraph out exactly as `-w G+10 -g G` does. - for goal in [5, 10, 20, 30, 50, 65] { - let widened = new_ucmd!() - .args(&[ - "one-word-per-line.txt", - "-w", - &(goal + 10).to_string(), - "-g", - &goal.to_string(), - ]) - .succeeds() - .stdout_move_str(); - - new_ucmd!() - .args(&["one-word-per-line.txt", "-g", &goal.to_string()]) - .succeeds() - .stdout_is(&widened); - } - - // The whole 37-column paragraph therefore fits on one line at goal 30. - new_ucmd!() - .args(&["one-word-per-line.txt", "--goal", "30"]) - .succeeds() - .stdout_is("this is a file with one word per line\n"); -} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index a114c61acac..817d1b4052e 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -5665,31 +5665,6 @@ fn test_ls_dired_order_format() { .stdout_contains("//DIRED//"); } -#[test] -fn test_ls_dired_offsets_follow_quoted_dir_headers() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; - at.mkdir("a b"); - at.touch("a b/x"); - at.mkdir("it's"); - at.touch("it's/y"); - - // Quoting lengthens the directory headers; both offset lists must follow - // the rendered header rather than the raw path. - let result = scene - .ucmd() - .args(&[ - "--dired", - "-R", - "--quoting-style=shell-escape", - "a b", - "it's", - ]) - .succeeds(); - assert_eq!(dired_names(result.stdout_str()), ["x", "y"]); - assert_eq!(subdired_names(result.stdout_str()), ["'a b'", "\"it's\""]); -} - #[test] fn test_ls_dired_and_zero_are_incompatible() { let scene = TestScenario::new(util_name!()); @@ -5942,19 +5917,10 @@ fn test_ls_dired_symlink_name_only() { /// Extracts the file names delimited by the //DIRED// byte offsets. fn dired_names(output: &str) -> Vec { - names_at_offsets(output, "//DIRED//") -} - -/// Extracts the directory headers delimited by the //SUBDIRED// byte offsets. -fn subdired_names(output: &str) -> Vec { - names_at_offsets(output, "//SUBDIRED//") -} - -fn names_at_offsets(output: &str, tag: &str) -> Vec { let dired_line = output .lines() - .find(|&line| line.starts_with(tag)) - .unwrap_or_else(|| panic!("no {tag} line in the output")); + .find(|&line| line.starts_with("//DIRED//")) + .unwrap(); let positions: Vec = dired_line .split_whitespace() .skip(1) @@ -7033,35 +6999,6 @@ fn test_ls_color_empty_style() { .stdout_only("\u{1b}[0mf\u{1b}[0m\n"); } -#[test] -fn test_ls_bad_ls_colors_is_an_error_not_a_warning() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; - at.touch("marker"); - - // A stray entry without '=' makes the whole variable unparsable; the - // diagnostic is an error, so it must not carry a "warning: " prefix. - scene - .ucmd() - .env("LS_COLORS", "di=1;35:stray") - .arg("--color=always") - .arg("marker") - .succeeds() - .stdout_is("marker\n") - .stderr_is("ls: unparsable value for LS_COLORS environment variable\n"); - - scene - .ucmd() - .env("LS_COLORS", "qq=1;35:stray") - .arg("--color=always") - .arg("marker") - .succeeds() - .stdout_is("marker\n") - .stderr_is( - "ls: unrecognized prefix: 'qq'\nls: unparsable value for LS_COLORS environment variable\n", - ); -} - #[test] fn test_ls_color_clear_to_eol() { let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_md5sum.rs b/tests/by-util/test_md5sum.rs index 2efb66073f9..48016e5e52d 100644 --- a/tests/by-util/test_md5sum.rs +++ b/tests/by-util/test_md5sum.rs @@ -3,12 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore checkfile, testf, ntestf - use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; - +// spell-checker:ignore checkfile, testf, ntestf macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index d2b41f72415..13d43f5a59a 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -80,7 +80,7 @@ fn test_create_one_fifo_already_exists() { .arg("abcdef") .arg("abcdef") .fails() - .stderr_contains("mkfifo: cannot create fifo 'abcdef': File exists"); + .stderr_is("mkfifo: cannot create fifo 'abcdef': File exists\n"); } #[test] diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 2a33c8d9c2f..0e9fa1dd949 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -10,6 +10,8 @@ use rstest::rstest; use std::io::Write; #[cfg(not(windows))] use std::path::Path; +#[cfg(target_os = "linux")] +use std::path::PathBuf; #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] use uucore::selinux::get_getfattr_output; use uutests::new_ucmd; @@ -3229,6 +3231,217 @@ fn test_mv_cross_device_dir_xattr_preserved() { assert_eq!(out.stdout, b"dirvalue"); } +/// Size of an attribute value that the `/dev/shm` (tmpfs) filesystem accepts +/// while the given destination directory rejects it, plus the value itself. +/// This is what makes the first xattr fail on the destination but not on the +/// source. Returns `None` when this machine's filesystem combination cannot +/// produce that failure, in which case the test should be skipped. +#[cfg(target_os = "linux")] +fn tmpfs_to_target_failing_xattr_value(dest_dir: &Path) -> Option { + use std::process::Command; + + for size in [9_100, 40_000] { + let value = "y".repeat(size); + let source_probe = Path::new("/dev/shm").join(format!("xattr_probe_{size}")); + let dest_probe = dest_dir.join(format!("probe_{size}")); + std::fs::write(&source_probe, "x").ok(); + std::fs::write(&dest_probe, "x").ok(); + let source_accepts = Command::new("setfattr") + .args(["-n", "user.huge", "-v", &value]) + .arg(&source_probe) + .status() + .is_ok_and(|s| s.success()); + let dest_rejects = !Command::new("setfattr") + .args(["-n", "user.huge", "-v", &value]) + .arg(&dest_probe) + .status() + .is_ok_and(|s| s.success()); + std::fs::remove_file(&source_probe).ok(); + std::fs::remove_file(&dest_probe).ok(); + if source_accepts && dest_rejects { + return Some(value); + } + } + None +} + +/// A failed xattr on a cross-device move must not stop the remaining +/// attributes from being copied: surviving attributes must still make it even +/// though `user.m_big` is rejected by the destination fs. The move +/// itself succeeds, GNU reports the failure on stderr and exits 0. +#[test] +#[cfg(target_os = "linux")] +fn test_mv_cross_device_xattr_partial_failure_keeps_remaining() { + use std::process::Command; + + let pid = std::process::id(); + let source_dir = Path::new("/dev/shm").join(format!("mv_xattr_partial_{pid}")); + let dest_dir = + PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(format!("mv_xattr_partial_{pid}")); + if std::fs::create_dir(&source_dir).is_err() || std::fs::create_dir(&dest_dir).is_err() { + return; // skip: no usable /dev/shm or target/tmp + } + let Some(big_value) = tmpfs_to_target_failing_xattr_value(&dest_dir) else { + std::fs::remove_dir_all(&source_dir).ok(); + std::fs::remove_dir_all(&dest_dir).ok(); + return; // skip: this filesystem combination cannot produce the failure + }; + + // Set small attributes around the failing big attribute so that + // regardless of filesystem listing order (alphabetical, insertion, + // or reverse-insertion), at least one surviving attribute is + // processed after the failing one. + let source = source_dir.join("src"); + std::fs::write(&source, "data").unwrap(); + Command::new("setfattr") + .args(["-n", "user.a_small", "-v", "12345678"]) + .arg(&source) + .status() + .unwrap(); + Command::new("setfattr") + .args(["-n", "user.m_big", "-v", &big_value]) + .arg(&source) + .status() + .unwrap(); + Command::new("setfattr") + .args(["-n", "user.z_small", "-v", "87654321"]) + .arg(&source) + .status() + .unwrap(); + + let dest = dest_dir.join("dst"); + let scene = TestScenario::new(util_name!()); + scene + .ucmd() + .arg(&source) + .arg(&dest) + .succeeds() + .stderr_contains("setting attribute 'user.m_big'"); + assert!( + !source.exists(), + "the source must be removed even when an xattr fails" + ); + + let small_a_out = Command::new("getfattr") + .args(["-n", "user.a_small", "--only-values", "--absolute-names"]) + .arg(&dest) + .output() + .expect("getfattr failed"); + assert!( + small_a_out.status.success(), + "user.a_small was lost on the destination: {}", + String::from_utf8_lossy(&small_a_out.stderr) + ); + assert_eq!(small_a_out.stdout, b"12345678"); + + let small_z_out = Command::new("getfattr") + .args(["-n", "user.z_small", "--only-values", "--absolute-names"]) + .arg(&dest) + .output() + .expect("getfattr failed"); + assert!( + small_z_out.status.success(), + "user.z_small was lost on the destination: {}", + String::from_utf8_lossy(&small_z_out.stderr) + ); + assert_eq!(small_z_out.stdout, b"87654321"); + + let big_out = Command::new("getfattr") + .args(["-n", "user.m_big", "--only-values", "--absolute-names"]) + .arg(&dest) + .output() + .expect("getfattr failed"); + assert!( + !big_out.status.success(), + "user.m_big should have been rejected by the destination fs" + ); + + std::fs::remove_dir_all(&source_dir).ok(); + std::fs::remove_dir_all(&dest_dir).ok(); +} + +/// The same partial-failure behavior must apply to a cross-device directory +/// move: the directory's own surviving xattrs are preserved even when the +/// failing `user.m_big` is rejected. +#[test] +#[cfg(target_os = "linux")] +fn test_mv_cross_device_dir_xattr_partial_failure_completes() { + use std::process::Command; + + let pid = std::process::id(); + let source_dir = Path::new("/dev/shm").join(format!("mv_dir_xattr_partial_{pid}")); + let dest_dir = + PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(format!("mv_dir_xattr_partial_{pid}")); + if std::fs::create_dir(&source_dir).is_err() || std::fs::create_dir(&dest_dir).is_err() { + return; // skip: no usable /dev/shm or target/tmp + } + let Some(big_value) = tmpfs_to_target_failing_xattr_value(&dest_dir) else { + std::fs::remove_dir_all(&source_dir).ok(); + std::fs::remove_dir_all(&dest_dir).ok(); + return; // skip: this filesystem combination cannot produce the failure + }; + + std::fs::write(source_dir.join("f.txt"), "content").unwrap(); + Command::new("setfattr") + .args(["-n", "user.a_small", "-v", "12345678"]) + .arg(&source_dir) + .status() + .unwrap(); + Command::new("setfattr") + .args(["-n", "user.m_big", "-v", &big_value]) + .arg(&source_dir) + .status() + .unwrap(); + Command::new("setfattr") + .args(["-n", "user.z_small", "-v", "87654321"]) + .arg(&source_dir) + .status() + .unwrap(); + + let dest = dest_dir.join("dst_dir"); + let scene = TestScenario::new(util_name!()); + scene + .ucmd() + .arg(&source_dir) + .arg(&dest) + .succeeds() + .stderr_contains("setting attribute 'user.m_big'"); + assert!( + !source_dir.exists(), + "the source directory must be removed even when an xattr fails" + ); + assert!( + dest.join("f.txt").exists(), + "directory contents must survive" + ); + + let small_a_out = Command::new("getfattr") + .args(["-n", "user.a_small", "--only-values", "--absolute-names"]) + .arg(&dest) + .output() + .expect("getfattr failed"); + assert!( + small_a_out.status.success(), + "directory user.a_small xattr was lost: {}", + String::from_utf8_lossy(&small_a_out.stderr) + ); + assert_eq!(small_a_out.stdout, b"12345678"); + + let small_z_out = Command::new("getfattr") + .args(["-n", "user.z_small", "--only-values", "--absolute-names"]) + .arg(&dest) + .output() + .expect("getfattr failed"); + assert!( + small_z_out.status.success(), + "directory user.z_small xattr was lost: {}", + String::from_utf8_lossy(&small_z_out.stderr) + ); + assert_eq!(small_z_out.stdout, b"87654321"); + + std::fs::remove_dir_all(&dest_dir).ok(); +} + /// Cross-device mv of a symlink onto an existing file must replace the /// destination atomically, matching GNU. #[test] diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 8868bca7caa..6da0c4964e0 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -441,23 +441,6 @@ fn test_width() { .stdout_only(expected_output); } -#[test] -fn test_large_width_ascii_dump() { - // A line is padded up to the full width before the ascii dump, so the - // output for a single byte is 4 * WIDTH + 21 characters wide. Checks that - // such a line comes out intact; the memory behavior at widths that cannot - // be buffered at all is covered by the GNU test suite (od/big-w.sh). - const WIDTH: usize = 4_000_000; - - let mut cmd = new_ucmd!(); - let result = cmd - .args(&[format!("-w{WIDTH}"), "-tcz".into()]) - .run_piped_stdin(&b"x"[..]); - let stdout = result.success().stdout_str(); - assert_eq!(stdout.len(), 4 * WIDTH + 21); - assert!(stdout.ends_with(" >x<\n0000001\n")); -} - #[test] fn test_invalid_width() { let input: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; @@ -597,7 +580,6 @@ fn test_suppress_duplicates() { .arg("-w4") .arg("-O") .arg("-x") - .arg("--endian=little") .run_piped_stdin(&input[..]) .success() .stdout_only(expected_output); @@ -1290,7 +1272,6 @@ fn test_od_options_after_filename() { .arg("-An") .arg("-t") .arg("x2") - .arg("--endian=little") .succeeds() .stdout_only(" 1c68 fdbb\n"); } diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 322274265fa..4d9c8ca7e36 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore bsdutils toybox - #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStringExt; use uutests::at_and_ucmd; diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index d0c96c6b196..6f52610341b 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -447,11 +447,9 @@ fn test_symlink_dir() { let at = &scene.fixtures; let dir = "test_rm_symlink_dir_directory"; - let file = "test_rm_symlink_dir_directory/file"; let link = "test_rm_symlink_dir_link"; at.mkdir(dir); - at.touch(file); at.symlink_dir(dir, link); scene @@ -463,9 +461,6 @@ fn test_symlink_dir() { assert!(at.dir_exists(link)); scene.ucmd().arg("-r").arg(link).succeeds(); - assert!(!at.dir_exists(link)); - assert!(at.dir_exists(dir)); - assert!(at.file_exists(file)); } #[test] diff --git a/tests/by-util/test_sha1sum.rs b/tests/by-util/test_sha1sum.rs index 80096a8eff2..d1c5864238b 100644 --- a/tests/by-util/test_sha1sum.rs +++ b/tests/by-util/test_sha1sum.rs @@ -3,12 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore checkfile, testf, ntestf - use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; - +// spell-checker:ignore checkfile, testf, ntestf macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] diff --git a/tests/by-util/test_sha224sum.rs b/tests/by-util/test_sha224sum.rs index a7009a0c4eb..d6abc05962e 100644 --- a/tests/by-util/test_sha224sum.rs +++ b/tests/by-util/test_sha224sum.rs @@ -3,10 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore checkfile, testf, ntestf - use uutests::new_ucmd; - +// spell-checker:ignore checkfile, testf, ntestf macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] diff --git a/tests/by-util/test_sha256sum.rs b/tests/by-util/test_sha256sum.rs index a1404a9ccd8..81698cfc26b 100644 --- a/tests/by-util/test_sha256sum.rs +++ b/tests/by-util/test_sha256sum.rs @@ -3,12 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore checkfile, testf, ntestf, heic - use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; - +// spell-checker:ignore checkfile, testf, ntestf, heic macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] diff --git a/tests/by-util/test_sha384sum.rs b/tests/by-util/test_sha384sum.rs index 53edccbad65..7b51995e478 100644 --- a/tests/by-util/test_sha384sum.rs +++ b/tests/by-util/test_sha384sum.rs @@ -3,10 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore checkfile, testf, ntestf - use uutests::new_ucmd; - +// spell-checker:ignore checkfile, testf, ntestf macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] diff --git a/tests/by-util/test_sha512sum.rs b/tests/by-util/test_sha512sum.rs index 9836cf4df4f..61c876f3215 100644 --- a/tests/by-util/test_sha512sum.rs +++ b/tests/by-util/test_sha512sum.rs @@ -3,10 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore checkfile, testf, ntestf - use uutests::new_ucmd; - +// spell-checker:ignore checkfile, testf, ntestf macro_rules! get_hash( ($str:expr) => ( $str.split(' ').collect::>()[0] diff --git a/tests/by-util/test_sleep.rs b/tests/by-util/test_sleep.rs index e44c114e433..f1a0381050d 100644 --- a/tests/by-util/test_sleep.rs +++ b/tests/by-util/test_sleep.rs @@ -3,11 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore dont SIGBUS SIGSEGV sigsegv sigbus infd - use rstest::rstest; use uucore::display::Quotable; +// spell-checker:ignore dont SIGBUS SIGSEGV sigsegv sigbus infd use uutests::new_ucmd; #[cfg(unix)] diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index e3d9552dc42..0ac09775681 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. // spell-checker:ignore (words) ints (linux) Nofile dfgi abmon avril - #![allow(clippy::cast_possible_wrap)] use std::env; diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index cf795ef5590..cadedc5c8ab 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -7,7 +7,6 @@ // spell-checker:ignore (libs) kqueue ELOOP EISDIR // spell-checker:ignore (jargon) tailable untailable datasame runneradmin tmpi // spell-checker:ignore (cmd) taskkill - #![allow( clippy::unicode_not_nfc, clippy::cast_lossless, diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index f8bad5aa834..b4894c5ed82 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -3,8 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore nopipe - #![allow(clippy::borrow_as_ptr)] use uutests::{at_and_ucmd, new_ucmd}; @@ -17,6 +15,8 @@ use std::time::Duration; // inspired by: // https://github.com/coreutils/coreutils/tests/misc/tee.sh +// spell-checker:ignore nopipe + #[test] #[cfg(unix)] fn test_error_stdin_directory() { diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 1690d1be836..de8b636bfc0 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -1588,26 +1588,19 @@ fn test_broken_pipe_no_error() { #[cfg(unix)] #[test] fn test_stdin_is_socket() { - use std::fs::File; use std::io::Write as _; - let (mut writer, reader): (File, File) = { - rustix::net::socketpair( - rustix::net::AddressFamily::UNIX, - rustix::net::SocketType::STREAM, - rustix::net::SocketFlags::empty(), - None, - ) - .map(|(fd0, fd1)| (fd0.into(), fd1.into())) - } + let (fd1, fd2) = rustix::net::socketpair( + rustix::net::AddressFamily::UNIX, + rustix::net::SocketType::STREAM, + rustix::net::SocketFlags::empty(), + None, + ) .unwrap(); - - writer.write_all(b"::").unwrap(); - drop(writer); - + std::fs::File::from(fd1).write_all(b"::").unwrap(); new_ucmd!() .args(&[":", ";"]) - .set_stdin(reader) + .set_stdin(fd2) .succeeds() .stdout_is(";;"); } diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index 675b83f3556..01042e9445a 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -313,7 +313,7 @@ fn test_tabs_with_invalid_chars() { #[test] fn test_tabs_shortcut_with_too_large_size() { let arg = format!("-{}", u128::MAX); - let expected_error = "tab stop is too large"; + let expected_error = "tab stop value is too large"; new_ucmd!().arg(arg).fails().stderr_contains(expected_error); } @@ -325,7 +325,7 @@ fn test_extended_tabstop_increment_overflow() { new_ucmd!() .arg(arg) .fails() - .stderr_contains("tab stop is too large"); + .stderr_contains("tab stop value is too large"); } #[test] diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 16fd79293f5..fc3bd861b3d 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -3,13 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (flags) lwmcL clmwL ; (path) bogusfile emptyfile manyemptylines moby notrailingnewline onelongemptyline onelongword weirdchars ioerrdir - #[cfg(unix)] use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::vec_of_size; +// spell-checker:ignore (flags) lwmcL clmwL ; (path) bogusfile emptyfile manyemptylines moby notrailingnewline onelongemptyline onelongword weirdchars ioerrdir #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh index e6adeb76035..41b9a49b4be 100755 --- a/util/fetch-gnu.sh +++ b/util/fetch-gnu.sh @@ -1,13 +1,17 @@ #!/bin/bash -e -ver="9.12" +ver="9.11" repo=https://github.com/coreutils/coreutils curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --strip-components=1 -xJf - # TODO stop backporting tests from master at GNU coreutils > $ver backport=( - # https://github.com/coreutils/coreutils/issues/355 - env/env.sh - env/printenv.sh + cat/splice.sh # split tests + dd/fail-ftruncate-fstat.sh # remove LD_PRELOAD + dd/stderr.sh # replace GNU/test binary by uutils/test + misc/close-stdout.sh # fix hardcoded pathes to GNU executables + misc/uname-labeled.sh # uname -A/--all-labeled, added after $ver + nproc/nproc-quota.sh # remove LD_PRELOAD + misc/empty-backup-suffix.sh ) for f in "${backport[@]}" do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f diff --git a/util/gnu-patches/error_msg_uniq.diff b/util/gnu-patches/error_msg_uniq.diff index cfac53c5c90..1c9e32fcb22 100644 --- a/util/gnu-patches/error_msg_uniq.diff +++ b/util/gnu-patches/error_msg_uniq.diff @@ -53,6 +53,6 @@ Index: gnu/tests/uniq/uniq.pl + {ERR=>"error: invalid value 'badoption' for '--group[=]'\n\n" . + " [possible values: separate, prepend, append, both]\n\n" . + "For more information, try '--help'.\n"}], - # Test for read buffer overrun. - do { my $longline = "\360\237\230\200" . "A" x 255 . "\n"; - ['146', '-w256', {IN => $longline x 2}, {OUT => $longline}] }, + ); + + # Locale related tests diff --git a/util/gnu-patches/tests_comm.pl.patch b/util/gnu-patches/tests_comm.pl.patch index 989ac441a50..4b683cc893a 100644 --- a/util/gnu-patches/tests_comm.pl.patch +++ b/util/gnu-patches/tests_comm.pl.patch @@ -1,7 +1,7 @@ -Index: gnu/tests/comm/comm.pl +Index: gnu/tests/misc/comm.pl =================================================================== ---- gnu.orig/tests/comm/comm.pl -+++ gnu/tests/comm/comm.pl +--- gnu.orig/tests/misc/comm.pl ++++ gnu/tests/misc/comm.pl @@ -73,18 +73,24 @@ my @Tests = # invalid missing command line argument (1) diff --git a/util/gnu-patches/tests_env_env-S.pl.patch b/util/gnu-patches/tests_env_env-S.pl.patch index 2ff931bc38a..955ed2c1c3f 100644 --- a/util/gnu-patches/tests_env_env-S.pl.patch +++ b/util/gnu-patches/tests_env_env-S.pl.patch @@ -36,7 +36,7 @@ Index: gnu/tests/env/env-S.pl + "For more information, try '--help'.\n" . + "$prog: use -[v]S to pass options in shebang lines\n"}], ['err_sp3', q['-v -S cat -n'], {EXIT=>125}, # embedded tab after -v -- {ERR=>"env: invalid option -- '\\t'\n" . +- {ERR=>"env: invalid option -- '\t'\n" . - "env: use -[v]S to pass options in shebang lines\n" . - "Try 'env --help' for more information.\n"}], + {ERR=>"error: unexpected argument '-\t' found\n\n" . diff --git a/util/gnu-patches/tests_ls_no_cap.patch b/util/gnu-patches/tests_ls_no_cap.patch index a8db18930c7..62b836f7962 100644 --- a/util/gnu-patches/tests_ls_no_cap.patch +++ b/util/gnu-patches/tests_ls_no_cap.patch @@ -6,15 +6,15 @@ index 99f0563bc..f7b9e7885 100755 skip_ "setcap doesn't work" LS_COLORS=ca=1; export LS_COLORS --strace -e trace=capget ls --color=always > /dev/null 2> out || fail=1 +-strace -e capget ls --color=always > /dev/null 2> out || fail=1 -$EGREP 'capget\(' out || skip_ "your ls doesn't call capget" -+strace -e trace=listxattr ls --color=always > /dev/null 2> out || fail=1 ++strace -e listxattr ls --color=always > /dev/null 2> out || fail=1 +$EGREP 'listxattr\(' out || skip_ "your ls doesn't call listxattr" LS_COLORS=ca=:; export LS_COLORS --strace -e trace=capget ls --color=always > /dev/null 2> out || fail=1 +-strace -e capget ls --color=always > /dev/null 2> out || fail=1 -$EGREP 'capget\(' out && fail=1 -+strace -e trace=listxattr ls --color=always > /dev/null 2> out || fail=1 ++strace -e listxattr ls --color=always > /dev/null 2> out || fail=1 +$EGREP 'listxattr\(' out && fail=1 Exit $fail diff --git a/util/gnu-patches/tests_pwd-long.patch b/util/gnu-patches/tests_pwd-long.patch index d5291137c27..a26847e26f7 100644 --- a/util/gnu-patches/tests_pwd-long.patch +++ b/util/gnu-patches/tests_pwd-long.patch @@ -4,19 +4,19 @@ Index: gnu/tests/pwd/pwd-long.sh +++ gnu/tests/pwd/pwd-long.sh @@ -19,7 +19,6 @@ - . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src + . "${srcdir=.}/tests/init.sh"; print_ver_ pwd -uses_strace_ require_readable_root_ require_perl_ -@@ -27,11 +26,10 @@ require_perl_ +@@ -27,11 +26,10 @@ ARGV_0=$0 export ARGV_0 -# Disable the getcwd syscall if possible, so more of our code is exercised. -no_sys_getcwd() { -- strace -f -o /dev/null -e trace=getcwd -e fault=all:error=ENOSYS "$@" +- strace -f -o /dev/null -e 'getcwd' -e fault=all:error=ENOSYS "$@" -} -no_sys_getcwd true || no_sys_getcwd() { "$@"; } +# uutils: our pwd has no userspace getcwd reimplementation like GNU's; it diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index 013119527b0..0480442e116 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -28,10 +28,7 @@ echo "path_UUTILS='${path_UUTILS}'" echo "path_GNU='${path_GNU}'" # Use GNU nproc for *BSD -NPROC_BIN=$(command -v "${path_GNU}"/src/nproc||command -v nproc) -# `-j` wants a job count: handing it the path to nproc made make read it as a -# (bogus) goal and run with unlimited parallelism, which starves the CI runner. -NPROC=$("${NPROC_BIN}" 2>/dev/null) || NPROC=1 +NPROC=$(command -v ${path_GNU}/src/nproc||command -v nproc) MAKEFLAGS="${MAKEFLAGS} -j ${NPROC}" export MAKEFLAGS ###