1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 19:17:43 +00:00

Merge branch 'main' into sort-mem-percent

This commit is contained in:
Sylvestre Ledru 2025-01-23 22:52:00 +01:00 committed by GitHub
commit 4f83924092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 890 additions and 411 deletions

View file

@ -417,14 +417,14 @@ jobs:
--arg multisize "$SIZE_MULTI" \ --arg multisize "$SIZE_MULTI" \
'{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json
- name: Download the previous individual size result - name: Download the previous individual size result
uses: dawidd6/action-download-artifact@v7 uses: dawidd6/action-download-artifact@v8
with: with:
workflow: CICD.yml workflow: CICD.yml
name: individual-size-result name: individual-size-result
repo: uutils/coreutils repo: uutils/coreutils
path: dl path: dl
- name: Download the previous size result - name: Download the previous size result
uses: dawidd6/action-download-artifact@v7 uses: dawidd6/action-download-artifact@v8
with: with:
workflow: CICD.yml workflow: CICD.yml
name: size-result name: size-result

View file

@ -91,7 +91,7 @@ jobs:
working-directory: ${{ steps.vars.outputs.path_GNU }} working-directory: ${{ steps.vars.outputs.path_GNU }}
- name: Retrieve reference artifacts - name: Retrieve reference artifacts
uses: dawidd6/action-download-artifact@v7 uses: dawidd6/action-download-artifact@v8
# ref: <https://github.com/dawidd6/action-download-artifact> # ref: <https://github.com/dawidd6/action-download-artifact>
continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet)
with: with:
@ -105,7 +105,7 @@ jobs:
run: | run: |
## Install dependencies ## Install dependencies
sudo apt-get update sudo apt-get update
sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt
- name: Add various locales - name: Add various locales
shell: bash shell: bash
run: | run: |

View file

@ -1,3 +1,5 @@
tests/tail/inotify-dir-recreate tests/tail/inotify-dir-recreate
tests/timeout/timeout tests/timeout/timeout
tests/rm/rm1 tests/rm/rm1
tests/misc/stdbuf
tests/misc/usage_vs_getopt

25
Cargo.lock generated
View file

@ -337,18 +337,18 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.26" version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
] ]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.26" version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -861,7 +861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -1276,7 +1276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -1996,7 +1996,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2241,7 +2241,7 @@ dependencies = [
"getrandom", "getrandom",
"once_cell", "once_cell",
"rustix 0.38.43", "rustix 0.38.43",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2617,9 +2617,7 @@ name = "uu_date"
version = "0.0.29" version = "0.0.29"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz",
"clap", "clap",
"iana-time-zone",
"libc", "libc",
"parse_datetime", "parse_datetime",
"uucore", "uucore",
@ -2879,11 +2877,9 @@ version = "0.0.29"
dependencies = [ dependencies = [
"ansi-width", "ansi-width",
"chrono", "chrono",
"chrono-tz",
"clap", "clap",
"glob", "glob",
"hostname", "hostname",
"iana-time-zone",
"lscolors", "lscolors",
"number_prefix", "number_prefix",
"once_cell", "once_cell",
@ -3472,6 +3468,8 @@ version = "0.0.29"
dependencies = [ dependencies = [
"blake2b_simd", "blake2b_simd",
"blake3", "blake3",
"chrono",
"chrono-tz",
"clap", "clap",
"crc32fast", "crc32fast",
"data-encoding", "data-encoding",
@ -3481,6 +3479,7 @@ dependencies = [
"dunce", "dunce",
"glob", "glob",
"hex", "hex",
"iana-time-zone",
"itertools 0.14.0", "itertools 0.14.0",
"lazy_static", "lazy_static",
"libc", "libc",
@ -3656,7 +3655,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View file

@ -576,6 +576,7 @@ semicolon_if_nothing_returned = "warn"
single_char_pattern = "warn" single_char_pattern = "warn"
explicit_iter_loop = "warn" explicit_iter_loop = "warn"
if_not_else = "warn" if_not_else = "warn"
manual_if_else = "warn"
all = { level = "deny", priority = -1 } all = { level = "deny", priority = -1 }
cargo = { level = "warn", priority = -1 } cargo = { level = "warn", priority = -1 }

View file

@ -241,6 +241,8 @@ DEBUG=1 bash util/run-gnu-test.sh tests/misc/sm3sum.pl
Note that GNU test suite relies on individual utilities (not the multicall binary). Note that GNU test suite relies on individual utilities (not the multicall binary).
You also need to install [quilt](https://savannah.nongnu.org/projects/quilt), a tool used to manage a stack of patches for modifying GNU tests.
On FreeBSD, you need to install packages for GNU coreutils and sed (used in shell scripts instead of system commands): On FreeBSD, you need to install packages for GNU coreutils and sed (used in shell scripts instead of system commands):
```shell ```shell

View file

@ -125,16 +125,13 @@ fn basename(fullname: &str, suffix: &str) -> String {
// Convert to path buffer and get last path component // Convert to path buffer and get last path component
let pb = PathBuf::from(path); let pb = PathBuf::from(path);
match pb.components().last() {
Some(c) => { pb.components().next_back().map_or_else(String::new, |c| {
let name = c.as_os_str().to_str().unwrap(); let name = c.as_os_str().to_str().unwrap();
if name == suffix { if name == suffix {
name.to_string() name.to_string()
} else { } else {
name.strip_suffix(suffix).unwrap_or(name).to_string() name.strip_suffix(suffix).unwrap_or(name).to_string()
} }
} })
None => String::new(),
}
} }

View file

@ -13,8 +13,9 @@ use std::iter;
use std::path::Path; use std::path::Path;
use uucore::checksum::{ use uucore::checksum::{
calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation, calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation,
ChecksumError, ChecksumOptions, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ChecksumError, ChecksumOptions, ChecksumVerbose, ALGORITHM_OPTIONS_BLAKE2B,
ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV,
SUPPORTED_ALGORITHMS,
}; };
use uucore::{ use uucore::{
encoding, encoding,
@ -322,13 +323,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(), || iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|files| files.map(OsStr::new).collect::<Vec<_>>(), |files| files.map(OsStr::new).collect::<Vec<_>>(),
); );
let verbose = ChecksumVerbose::new(status, quiet, warn);
let opts = ChecksumOptions { let opts = ChecksumOptions {
binary: binary_flag, binary: binary_flag,
ignore_missing, ignore_missing,
quiet,
status,
strict, strict,
warn, verbose,
}; };
return perform_checksum_validation(files.iter().copied(), algo_option, length, opts); return perform_checksum_validation(files.iter().copied(), algo_option, length, opts);
@ -462,19 +464,22 @@ pub fn uu_app() -> Command {
.short('w') .short('w')
.long("warn") .long("warn")
.help("warn about improperly formatted checksum lines") .help("warn about improperly formatted checksum lines")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue)
.overrides_with_all([options::STATUS, options::QUIET]),
) )
.arg( .arg(
Arg::new(options::STATUS) Arg::new(options::STATUS)
.long("status") .long("status")
.help("don't output anything, status code shows success") .help("don't output anything, status code shows success")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue)
.overrides_with_all([options::WARN, options::QUIET]),
) )
.arg( .arg(
Arg::new(options::QUIET) Arg::new(options::QUIET)
.long(options::QUIET) .long(options::QUIET)
.help("don't print OK for each successfully verified file") .help("don't print OK for each successfully verified file")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue)
.overrides_with_all([options::WARN, options::STATUS]),
) )
.arg( .arg(
Arg::new(options::IGNORE_MISSING) Arg::new(options::IGNORE_MISSING)

View file

@ -20,10 +20,8 @@ path = "src/date.rs"
[dependencies] [dependencies]
chrono = { workspace = true } chrono = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
uucore = { workspace = true } uucore = { workspace = true, features = ["custom-tz-fmt"] }
parse_datetime = { workspace = true } parse_datetime = { workspace = true }
chrono-tz = { workspace = true }
iana-time-zone = { workspace = true }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = { workspace = true } libc = { workspace = true }

View file

@ -6,17 +6,16 @@
// spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes // spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes
use chrono::format::{Item, StrftimeItems}; use chrono::format::{Item, StrftimeItems};
use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, TimeZone, Utc}; use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc};
#[cfg(windows)] #[cfg(windows)]
use chrono::{Datelike, Timelike}; use chrono::{Datelike, Timelike};
use chrono_tz::{OffsetName, Tz};
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use iana_time_zone::get_timezone;
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))]
use libc::{clock_settime, timespec, CLOCK_REALTIME}; use libc::{clock_settime, timespec, CLOCK_REALTIME};
use std::fs::File; use std::fs::File;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::path::PathBuf; use std::path::PathBuf;
use uucore::custom_tz_fmt::custom_time_format;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::FromIo; use uucore::error::FromIo;
use uucore::error::{UResult, USimpleError}; use uucore::error::{UResult, USimpleError};
@ -274,21 +273,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
for date in dates { for date in dates {
match date { match date {
Ok(date) => { Ok(date) => {
// TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 let format_string = custom_time_format(format_string);
let tz = match std::env::var("TZ") {
// TODO Support other time zones...
Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC,
_ => match get_timezone() {
Ok(tz_str) => tz_str.parse().unwrap(),
Err(_) => Tz::Etc__UTC,
},
};
let offset = tz.offset_from_utc_date(&Utc::now().date_naive());
let tz_abbreviation = offset.abbreviation();
// GNU `date` uses `%N` for nano seconds, however crate::chrono uses `%f`
let format_string = &format_string
.replace("%N", "%f")
.replace("%Z", tz_abbreviation.unwrap_or("UTC"));
// Refuse to pass this string to chrono as it is crashing in this crate // Refuse to pass this string to chrono as it is crashing in this crate
if format_string.contains("%#z") { if format_string.contains("%#z") {
return Err(USimpleError::new( return Err(USimpleError::new(
@ -298,7 +283,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} }
// Hack to work around panic in chrono, // Hack to work around panic in chrono,
// TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released // TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released
let format_items = StrftimeItems::new(format_string); let format_items = StrftimeItems::new(format_string.as_str());
if format_items.clone().any(|i| i == Item::Error) { if format_items.clone().any(|i| i == Item::Error) {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 1,

View file

@ -54,7 +54,7 @@ fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool {
let last_mount_for_dir = mounts let last_mount_for_dir = mounts
.iter() .iter()
.filter(|m| m.mount_dir == mount.mount_dir) .filter(|m| m.mount_dir == mount.mount_dir)
.last(); .next_back();
if let Some(lmi) = last_mount_for_dir { if let Some(lmi) = last_mount_for_dir {
lmi.dev_name != mount.dev_name lmi.dev_name != mount.dev_name

View file

@ -130,14 +130,11 @@ fn parse_signal_opt<'a>(opts: &mut Options<'a>, opt: &'a OsStr) -> UResult<()> {
} }
}); });
for sig in sig_vec { for sig in sig_vec {
let sig_str = match sig.to_str() { let Some(sig_str) = sig.to_str() else {
Some(s) => s,
None => {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 1,
format!("{}: invalid signal", sig.quote()), format!("{}: invalid signal", sig.quote()),
)) ));
}
}; };
let sig_val = parse_signal_value(sig_str)?; let sig_val = parse_signal_value(sig_str)?;
if !opts.ignore_signal.contains(&sig_val) { if !opts.ignore_signal.contains(&sig_val) {

View file

@ -255,9 +255,8 @@ impl ParagraphStream<'_> {
if l_slice.starts_with("From ") { if l_slice.starts_with("From ") {
true true
} else { } else {
let colon_posn = match l_slice.find(':') { let Some(colon_posn) = l_slice.find(':') else {
Some(n) => n, return false;
None => return false,
}; };
// header field must be nonzero length // header field must be nonzero length
@ -560,12 +559,11 @@ impl<'a> Iterator for WordSplit<'a> {
// find the start of the next word, and record if we find a tab character // find the start of the next word, and record if we find a tab character
let (before_tab, after_tab, word_start) = let (before_tab, after_tab, word_start) =
match self.analyze_tabs(&self.string[old_position..]) { if let (b, a, Some(s)) = self.analyze_tabs(&self.string[old_position..]) {
(b, a, Some(s)) => (b, a, s + old_position), (b, a, s + old_position)
(_, _, None) => { } else {
self.position = self.length; self.position = self.length;
return None; return None;
}
}; };
// find the beginning of the next whitespace // find the beginning of the next whitespace

View file

@ -24,6 +24,7 @@ use uucore::checksum::escape_filename;
use uucore::checksum::perform_checksum_validation; use uucore::checksum::perform_checksum_validation;
use uucore::checksum::ChecksumError; use uucore::checksum::ChecksumError;
use uucore::checksum::ChecksumOptions; use uucore::checksum::ChecksumOptions;
use uucore::checksum::ChecksumVerbose;
use uucore::checksum::HashAlgorithm; use uucore::checksum::HashAlgorithm;
use uucore::error::{FromIo, UResult}; use uucore::error::{FromIo, UResult};
use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256};
@ -240,13 +241,14 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
|| iter::once(OsStr::new("-")).collect::<Vec<_>>(), || iter::once(OsStr::new("-")).collect::<Vec<_>>(),
|files| files.map(OsStr::new).collect::<Vec<_>>(), |files| files.map(OsStr::new).collect::<Vec<_>>(),
); );
let verbose = ChecksumVerbose::new(status, quiet, warn);
let opts = ChecksumOptions { let opts = ChecksumOptions {
binary, binary,
ignore_missing, ignore_missing,
quiet,
status,
strict, strict,
warn, verbose,
}; };
// Execute the checksum validation // Execute the checksum validation
@ -356,14 +358,16 @@ pub fn uu_app_common() -> Command {
.short('q') .short('q')
.long(options::QUIET) .long(options::QUIET)
.help("don't print OK for each successfully verified file") .help("don't print OK for each successfully verified file")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue)
.overrides_with_all([options::STATUS, options::WARN]),
) )
.arg( .arg(
Arg::new(options::STATUS) Arg::new(options::STATUS)
.short('s') .short('s')
.long("status") .long("status")
.help("don't output anything, status code shows success") .help("don't output anything, status code shows success")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue)
.overrides_with_all([options::QUIET, options::WARN]),
) )
.arg( .arg(
Arg::new(options::STRICT) Arg::new(options::STRICT)
@ -382,7 +386,8 @@ pub fn uu_app_common() -> Command {
.short('w') .short('w')
.long("warn") .long("warn")
.help("warn about improperly formatted checksum lines") .help("warn about improperly formatted checksum lines")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue)
.overrides_with_all([options::QUIET, options::STATUS]),
) )
.arg( .arg(
Arg::new("zero") Arg::new("zero")

View file

@ -91,9 +91,8 @@ fn process_num_block(
} }
if let Some(n) = multiplier { if let Some(n) = multiplier {
options.push(OsString::from("-c")); options.push(OsString::from("-c"));
let num = match num.checked_mul(n) { let Some(num) = num.checked_mul(n) else {
Some(n) => n, return Some(Err(ParseError::Overflow));
None => return Some(Err(ParseError::Overflow)),
}; };
options.push(OsString::from(format!("{num}"))); options.push(OsString::from(format!("{num}")));
} else { } else {

View file

@ -652,7 +652,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
} }
let mut targetpath = target_dir.to_path_buf(); let mut targetpath = target_dir.to_path_buf();
let filename = sourcepath.components().last().unwrap(); let filename = sourcepath.components().next_back().unwrap();
targetpath.push(filename); targetpath.push(filename);
show_if_err!(copy(sourcepath, &targetpath, b)); show_if_err!(copy(sourcepath, &targetpath, b));

View file

@ -154,12 +154,7 @@ fn handle_obsolete(args: &mut Vec<String>) -> Option<usize> {
} }
fn table() { fn table() {
// GNU kill doesn't list the EXIT signal with --table, so we ignore it, too for (idx, signal) in ALL_SIGNALS.iter().enumerate() {
for (idx, signal) in ALL_SIGNALS
.iter()
.enumerate()
.filter(|(_, s)| **s != "EXIT")
{
println!("{idx: >#2} {signal}"); println!("{idx: >#2} {signal}");
} }
} }
@ -183,8 +178,7 @@ fn print_signal(signal_name_or_value: &str) -> UResult<()> {
} }
fn print_signals() { fn print_signals() {
// GNU kill doesn't list the EXIT signal with --list, so we ignore it, too for signal in ALL_SIGNALS {
for signal in ALL_SIGNALS.iter().filter(|x| **x != "EXIT") {
println!("{signal}"); println!("{signal}");
} }
} }

View file

@ -19,11 +19,9 @@ path = "src/ls.rs"
[dependencies] [dependencies]
ansi-width = { workspace = true } ansi-width = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
chrono-tz = { workspace = true }
clap = { workspace = true, features = ["env"] } clap = { workspace = true, features = ["env"] }
glob = { workspace = true } glob = { workspace = true }
hostname = { workspace = true } hostname = { workspace = true }
iana-time-zone = { workspace = true }
lscolors = { workspace = true } lscolors = { workspace = true }
number_prefix = { workspace = true } number_prefix = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
@ -31,6 +29,7 @@ selinux = { workspace = true, optional = true }
terminal_size = { workspace = true } terminal_size = { workspace = true }
uucore = { workspace = true, features = [ uucore = { workspace = true, features = [
"colors", "colors",
"custom-tz-fmt",
"entries", "entries",
"format", "format",
"fs", "fs",

View file

@ -27,14 +27,12 @@ use std::{
use std::{collections::HashSet, io::IsTerminal}; use std::{collections::HashSet, io::IsTerminal};
use ansi_width::ansi_width; use ansi_width::ansi_width;
use chrono::{DateTime, Local, TimeDelta, TimeZone, Utc}; use chrono::{DateTime, Local, TimeDelta};
use chrono_tz::{OffsetName, Tz};
use clap::{ use clap::{
builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, builder::{NonEmptyStringValueParser, PossibleValue, ValueParser},
crate_version, Arg, ArgAction, Command, crate_version, Arg, ArgAction, Command,
}; };
use glob::{MatchOptions, Pattern}; use glob::{MatchOptions, Pattern};
use iana_time_zone::get_timezone;
use lscolors::LsColors; use lscolors::LsColors;
use term_grid::{Direction, Filling, Grid, GridOptions}; use term_grid::{Direction, Filling, Grid, GridOptions};
@ -60,6 +58,7 @@ use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
use uucore::line_ending::LineEnding; use uucore::line_ending::LineEnding;
use uucore::quoting_style::{self, escape_name, QuotingStyle}; use uucore::quoting_style::{self, escape_name, QuotingStyle};
use uucore::{ use uucore::{
custom_tz_fmt,
display::Quotable, display::Quotable,
error::{set_exit_code, UError, UResult}, error::{set_exit_code, UError, UResult},
format_usage, format_usage,
@ -345,31 +344,6 @@ fn is_recent(time: DateTime<Local>) -> bool {
time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now() time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now()
} }
/// Get the alphabetic abbreviation of the current timezone.
///
/// For example, "UTC" or "CET" or "PDT".
fn timezone_abbrev() -> String {
let tz = match std::env::var("TZ") {
// TODO Support other time zones...
Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC,
_ => match get_timezone() {
Ok(tz_str) => tz_str.parse().unwrap(),
Err(_) => Tz::Etc__UTC,
},
};
let offset = tz.offset_from_utc_date(&Utc::now().date_naive());
offset.abbreviation().unwrap_or("UTC").to_string()
}
/// Format the given time according to a custom format string.
fn custom_time_format(fmt: &str, time: DateTime<Local>) -> String {
// TODO Refactor the common code from `ls` and `date` for rendering dates.
// TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970
// GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`.
let fmt = fmt.replace("%N", "%f").replace("%Z", &timezone_abbrev());
time.format(&fmt).to_string()
}
impl TimeStyle { impl TimeStyle {
/// Format the given time according to this time format style. /// Format the given time according to this time format style.
fn format(&self, time: DateTime<Local>) -> String { fn format(&self, time: DateTime<Local>) -> String {
@ -386,7 +360,9 @@ impl TimeStyle {
//So it's not yet implemented //So it's not yet implemented
(Self::Locale, true) => time.format("%b %e %H:%M").to_string(), (Self::Locale, true) => time.format("%b %e %H:%M").to_string(),
(Self::Locale, false) => time.format("%b %e %Y").to_string(), (Self::Locale, false) => time.format("%b %e %Y").to_string(),
(Self::Format(e), _) => custom_time_format(e, time), (Self::Format(fmt), _) => time
.format(custom_tz_fmt::custom_time_format(fmt).as_str())
.to_string(),
} }
} }
} }
@ -403,8 +379,8 @@ fn parse_time_style(options: &clap::ArgMatches) -> Result<TimeStyle, LsError> {
//If both FULL_TIME and TIME_STYLE are present //If both FULL_TIME and TIME_STYLE are present
//The one added last is dominant //The one added last is dominant
if options.get_flag(options::FULL_TIME) if options.get_flag(options::FULL_TIME)
&& options.indices_of(options::FULL_TIME).unwrap().last() && options.indices_of(options::FULL_TIME).unwrap().next_back()
> options.indices_of(options::TIME_STYLE).unwrap().last() > options.indices_of(options::TIME_STYLE).unwrap().next_back()
{ {
Ok(TimeStyle::FullIso) Ok(TimeStyle::FullIso)
} else { } else {

View file

@ -101,8 +101,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let options = SeqOptions { let options = SeqOptions {
separator: matches separator: matches
.get_one::<String>(OPT_SEPARATOR) .get_one::<String>(OPT_SEPARATOR)
.map(|s| s.as_str()) .map_or("\n", |s| s.as_str())
.unwrap_or("\n")
.to_string(), .to_string(),
terminator: matches terminator: matches
.get_one::<String>(OPT_TERMINATOR) .get_one::<String>(OPT_TERMINATOR)
@ -150,13 +149,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let precision = select_precision(first_precision, increment_precision, last_precision); let precision = select_precision(first_precision, increment_precision, last_precision);
let format = match options.format { let format = options
Some(f) => { .format
let f = Format::<num_format::Float>::parse(f)?; .map(Format::<num_format::Float>::parse)
Some(f) .transpose()?;
}
None => None,
};
let result = print_seq( let result = print_seq(
(first.number, increment.number, last.number), (first.number, increment.number, last.number),
precision, precision,
@ -164,12 +161,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
&options.terminator, &options.terminator,
options.equal_width, options.equal_width,
padding, padding,
&format, format.as_ref(),
); );
match result { match result {
Ok(_) => Ok(()), Ok(()) => Ok(()),
Err(err) if err.kind() == ErrorKind::BrokenPipe => Ok(()), Err(err) if err.kind() == ErrorKind::BrokenPipe => Ok(()),
Err(e) => Err(e.map_err_context(|| "write error".into())), Err(err) => Err(err.map_err_context(|| "write error".into())),
} }
} }
@ -263,7 +260,7 @@ fn print_seq(
terminator: &str, terminator: &str,
pad: bool, pad: bool,
padding: usize, padding: usize,
format: &Option<Format<num_format::Float>>, format: Option<&Format<num_format::Float>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let stdout = stdout(); let stdout = stdout();
let mut stdout = stdout.lock(); let mut stdout = stdout.lock();

View file

@ -48,7 +48,7 @@ rand = "0.8.3"
```rust ```rust
use rand::prelude::*; use rand::prelude::*;
fn main() { fn main() {
let suffixes = ['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; let suffixes = ['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'];
let mut rng = thread_rng(); let mut rng = thread_rng();
for _ in 0..100000 { for _ in 0..100000 {
println!( println!(

View file

@ -224,11 +224,8 @@ fn read_write_loop<I: WriteableTmpFile>(
let mut sender_option = Some(sender); let mut sender_option = Some(sender);
let mut tmp_files = vec![]; let mut tmp_files = vec![];
loop { loop {
let chunk = match receiver.recv() { let Ok(chunk) = receiver.recv() else {
Ok(it) => it,
_ => {
return Ok(ReadResult::WroteChunksToFile { tmp_files }); return Ok(ReadResult::WroteChunksToFile { tmp_files });
}
}; };
let tmp_file = write::<I>( let tmp_file = write::<I>(

View file

@ -82,7 +82,10 @@ impl NumInfo {
if Self::is_invalid_char(char, &mut had_decimal_pt, parse_settings) { if Self::is_invalid_char(char, &mut had_decimal_pt, parse_settings) {
return if let Some(start) = start { return if let Some(start) = start {
let has_si_unit = parse_settings.accept_si_units let has_si_unit = parse_settings.accept_si_units
&& matches!(char, 'K' | 'k' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y'); && matches!(
char,
'K' | 'k' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | 'R' | 'Q'
);
( (
Self { exponent, sign }, Self { exponent, sign },
start..if has_si_unit { idx + 1 } else { idx }, start..if has_si_unit { idx + 1 } else { idx },
@ -176,6 +179,8 @@ fn get_unit(unit: Option<char>) -> u8 {
'E' => 6, 'E' => 6,
'Z' => 7, 'Z' => 7,
'Y' => 8, 'Y' => 8,
'R' => 9,
'Q' => 10,
_ => 0, _ => 0,
} }
} else { } else {

View file

@ -34,6 +34,7 @@ use std::ffi::{OsStr, OsString};
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
use std::num::IntErrorKind;
use std::ops::Range; use std::ops::Range;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@ -288,7 +289,7 @@ impl GlobalSettings {
// GNU sort (8.32) invalid: b, B, 1B, p, e, z, y // GNU sort (8.32) invalid: b, B, 1B, p, e, z, y
let size = Parser::default() let size = Parser::default()
.with_allow_list(&[ .with_allow_list(&[
"b", "k", "K", "m", "M", "g", "G", "t", "T", "P", "E", "Z", "Y", "%", "b", "k", "K", "m", "M", "g", "G", "t", "T", "P", "E", "Z", "Y", "R", "Q", "%",
]) ])
.with_default_unit("K") .with_default_unit("K")
.with_b_byte_count(true) .with_b_byte_count(true)
@ -534,8 +535,9 @@ impl<'a> Line<'a> {
} else { } else {
// include a trailing si unit // include a trailing si unit
if selector.settings.mode == SortMode::HumanNumeric if selector.settings.mode == SortMode::HumanNumeric
&& self.line[selection.end..initial_selection.end] && self.line[selection.end..initial_selection.end].starts_with(
.starts_with(&['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'][..]) &['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'][..],
)
{ {
selection.end += 1; selection.end += 1;
} }
@ -696,9 +698,17 @@ impl KeyPosition {
.ok_or_else(|| format!("invalid key {}", key.quote()))?; .ok_or_else(|| format!("invalid key {}", key.quote()))?;
let char = field_and_char.next(); let char = field_and_char.next();
let field = field let field = match field.parse::<usize>() {
.parse() Ok(f) => f,
.map_err(|e| format!("failed to parse field index {}: {}", field.quote(), e))?; Err(e) if *e.kind() == IntErrorKind::PosOverflow => usize::MAX,
Err(e) => {
return Err(format!(
"failed to parse field index {} {}",
field.quote(),
e
))
}
};
if field == 0 { if field == 0 {
return Err("field index can not be 0".to_string()); return Err("field index can not be 0".to_string());
} }
@ -1361,14 +1371,14 @@ pub fn uu_app() -> Command {
options::check::QUIET, options::check::QUIET,
options::check::DIAGNOSE_FIRST, options::check::DIAGNOSE_FIRST,
])) ]))
.conflicts_with(options::OUTPUT) .conflicts_with_all([options::OUTPUT, options::check::CHECK_SILENT])
.help("check for sorted input; do not sort"), .help("check for sorted input; do not sort"),
) )
.arg( .arg(
Arg::new(options::check::CHECK_SILENT) Arg::new(options::check::CHECK_SILENT)
.short('C') .short('C')
.long(options::check::CHECK_SILENT) .long(options::check::CHECK_SILENT)
.conflicts_with(options::OUTPUT) .conflicts_with_all([options::OUTPUT, options::check::CHECK])
.help( .help(
"exit successfully if the given file is already sorted, \ "exit successfully if the given file is already sorted, \
and exit with status 1 otherwise.", and exit with status 1 otherwise.",

View file

@ -1408,20 +1408,17 @@ where
}; };
let bytes = line.as_slice(); let bytes = line.as_slice();
match kth_chunk { if let Some(chunk_number) = kth_chunk {
Some(chunk_number) => {
if (i % num_chunks) == (chunk_number - 1) as usize { if (i % num_chunks) == (chunk_number - 1) as usize {
stdout_writer.write_all(bytes)?; stdout_writer.write_all(bytes)?;
} }
} } else {
None => {
let writer = out_files.get_writer(i % num_chunks, settings)?; let writer = out_files.get_writer(i % num_chunks, settings)?;
let writer_stdin_open = custom_write_all(bytes, writer, settings)?; let writer_stdin_open = custom_write_all(bytes, writer, settings)?;
if !writer_stdin_open { if !writer_stdin_open {
closed_writers += 1; closed_writers += 1;
} }
} }
}
i += 1; i += 1;
if closed_writers == num_chunks { if closed_writers == num_chunks {
// all writers are closed - stop reading // all writers are closed - stop reading

View file

@ -34,9 +34,8 @@ pub enum ParseError {
/// Parses obsolete syntax /// Parses obsolete syntax
/// tail -\[NUM\]\[bcl\]\[f\] and tail +\[NUM\]\[bcl\]\[f\] /// tail -\[NUM\]\[bcl\]\[f\] and tail +\[NUM\]\[bcl\]\[f\]
pub fn parse_obsolete(src: &OsString) -> Option<Result<ObsoleteArgs, ParseError>> { pub fn parse_obsolete(src: &OsString) -> Option<Result<ObsoleteArgs, ParseError>> {
let mut rest = match src.to_str() { let Some(mut rest) = src.to_str() else {
Some(src) => src, return Some(Err(ParseError::InvalidEncoding));
None => return Some(Err(ParseError::InvalidEncoding)),
}; };
let sign = if let Some(r) = rest.strip_prefix('-') { let sign = if let Some(r) = rest.strip_prefix('-') {
rest = r; rest = r;
@ -86,9 +85,8 @@ pub fn parse_obsolete(src: &OsString) -> Option<Result<ObsoleteArgs, ParseError>
} }
let multiplier = if mode == 'b' { 512 } else { 1 }; let multiplier = if mode == 'b' { 512 } else { 1 };
let num = match num.checked_mul(multiplier) { let Some(num) = num.checked_mul(multiplier) else {
Some(n) => n, return Some(Err(ParseError::Overflow));
None => return Some(Err(ParseError::Overflow)),
}; };
Some(Ok(ObsoleteArgs { Some(Ok(ObsoleteArgs {

View file

@ -210,13 +210,8 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> {
fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> { fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> {
// Don't manage the error. GNU doesn't show error when doing // Don't manage the error. GNU doesn't show error when doing
// test foo -nt bar // test foo -nt bar
let f_a = match fs::metadata(a) { let (Ok(f_a), Ok(f_b)) = (fs::metadata(a), fs::metadata(b)) else {
Ok(f) => f, return Ok(false);
Err(_) => return Ok(false),
};
let f_b = match fs::metadata(b) {
Ok(f) => f,
Err(_) => return Ok(false),
}; };
Ok(match op.to_str() { Ok(match op.to_str() {
@ -290,11 +285,8 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool {
fs::metadata(path) fs::metadata(path)
}; };
let metadata = match metadata { let Ok(metadata) = metadata else {
Ok(metadata) => metadata,
Err(_) => {
return false; return false;
}
}; };
let file_type = metadata.file_type(); let file_type = metadata.file_type();

View file

@ -129,6 +129,7 @@ pub fn uu_app() -> Command {
.arg( .arg(
Arg::new(options::FOREGROUND) Arg::new(options::FOREGROUND)
.long(options::FOREGROUND) .long(options::FOREGROUND)
.short('f')
.help( .help(
"when not running timeout directly from a shell prompt, allow \ "when not running timeout directly from a shell prompt, allow \
COMMAND to read from the TTY and get TTY signals; in this mode, \ COMMAND to read from the TTY and get TTY signals; in this mode, \
@ -148,6 +149,7 @@ pub fn uu_app() -> Command {
.arg( .arg(
Arg::new(options::PRESERVE_STATUS) Arg::new(options::PRESERVE_STATUS)
.long(options::PRESERVE_STATUS) .long(options::PRESERVE_STATUS)
.short('p')
.help("exit with the same status as COMMAND, even when the command times out") .help("exit with the same status as COMMAND, even when the command times out")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )

View file

@ -599,14 +599,11 @@ fn parse_timestamp(s: &str) -> UResult<FileTime> {
let local = NaiveDateTime::parse_from_str(&ts, format) let local = NaiveDateTime::parse_from_str(&ts, format)
.map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?; .map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?;
let mut local = match chrono::Local.from_local_datetime(&local) { let LocalResult::Single(mut local) = chrono::Local.from_local_datetime(&local) else {
LocalResult::Single(dt) => dt,
_ => {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 1,
format!("invalid date ts format {}", ts.quote()), format!("invalid date ts format {}", ts.quote()),
)) ));
}
}; };
// Chrono caps seconds at 59, but 60 is valid. It might be a leap second // Chrono caps seconds at 59, but 60 is valid. It might be a leap second

View file

@ -171,12 +171,9 @@ impl Uniq {
// Convert the leftover bytes to UTF-8 for character-based -w // Convert the leftover bytes to UTF-8 for character-based -w
// If invalid UTF-8, just compare them as individual bytes (fallback). // If invalid UTF-8, just compare them as individual bytes (fallback).
let string_after_skip = match std::str::from_utf8(fields_to_check) { let Ok(string_after_skip) = std::str::from_utf8(fields_to_check) else {
Ok(s) => s,
Err(_) => {
// Fallback: if invalid UTF-8, treat them as single-byte “chars” // Fallback: if invalid UTF-8, treat them as single-byte “chars”
return closure(&mut fields_to_check.iter().map(|&b| b as char)); return closure(&mut fields_to_check.iter().map(|&b| b as char));
}
}; };
let total_chars = string_after_skip.chars().count(); let total_chars = string_after_skip.chars().count();

View file

@ -18,6 +18,8 @@ edition = "2021"
path = "src/lib/lib.rs" path = "src/lib/lib.rs"
[dependencies] [dependencies]
chrono = { workspace = true }
chrono-tz = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
uucore_procs = { workspace = true } uucore_procs = { workspace = true }
number_prefix = { workspace = true } number_prefix = { workspace = true }
@ -25,6 +27,7 @@ dns-lookup = { workspace = true, optional = true }
dunce = { version = "1.0.4", optional = true } dunce = { version = "1.0.4", optional = true }
wild = "2.2.1" wild = "2.2.1"
glob = { workspace = true } glob = { workspace = true }
iana-time-zone = { workspace = true }
lazy_static = "1.4.0" lazy_static = "1.4.0"
# * optional # * optional
itertools = { workspace = true, optional = true } itertools = { workspace = true, optional = true }
@ -114,4 +117,5 @@ utf8 = []
utmpx = ["time", "time/macros", "libc", "dns-lookup"] utmpx = ["time", "time/macros", "libc", "dns-lookup"]
version-cmp = [] version-cmp = []
wide = [] wide = []
custom-tz-fmt = []
tty = [] tty = []

View file

@ -12,6 +12,8 @@ pub mod buf_copy;
pub mod checksum; pub mod checksum;
#[cfg(feature = "colors")] #[cfg(feature = "colors")]
pub mod colors; pub mod colors;
#[cfg(feature = "custom-tz-fmt")]
pub mod custom_tz_fmt;
#[cfg(feature = "encoding")] #[cfg(feature = "encoding")]
pub mod encoding; pub mod encoding;
#[cfg(feature = "format")] #[cfg(feature = "format")]

View file

@ -19,7 +19,7 @@ use std::{
}; };
use crate::{ use crate::{
error::{set_exit_code, FromIo, UError, UResult, USimpleError}, error::{FromIo, UError, UResult, USimpleError},
os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps,
sum::{ sum::{
Blake2b, Blake3, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha3_224, Blake2b, Blake3, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha3_224,
@ -130,10 +130,12 @@ impl From<ChecksumError> for LineCheckError {
enum FileCheckError { enum FileCheckError {
/// a generic UError was encountered in sub-functions /// a generic UError was encountered in sub-functions
UError(Box<dyn UError>), UError(Box<dyn UError>),
/// the checksum file is improperly formatted.
ImproperlyFormatted,
/// reading of the checksum file failed /// reading of the checksum file failed
CantOpenChecksumFile, CantOpenChecksumFile,
/// processing of the file is considered as a failure regarding the
/// provided flags. This however does not stop the processing of
/// further files.
Failed,
} }
impl From<Box<dyn UError>> for FileCheckError { impl From<Box<dyn UError>> for FileCheckError {
@ -148,15 +150,57 @@ impl From<ChecksumError> for FileCheckError {
} }
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
pub enum ChecksumVerbose {
Status,
Quiet,
Normal,
Warning,
}
impl ChecksumVerbose {
pub fn new(status: bool, quiet: bool, warn: bool) -> Self {
use ChecksumVerbose::*;
// Assume only one of the three booleans will be enabled at once.
// This is ensured by clap's overriding arguments.
match (status, quiet, warn) {
(true, _, _) => Status,
(_, true, _) => Quiet,
(_, _, true) => Warning,
_ => Normal,
}
}
#[inline]
pub fn over_status(self) -> bool {
self > Self::Status
}
#[inline]
pub fn over_quiet(self) -> bool {
self > Self::Quiet
}
#[inline]
pub fn at_least_warning(self) -> bool {
self >= Self::Warning
}
}
impl Default for ChecksumVerbose {
fn default() -> Self {
Self::Normal
}
}
/// This struct regroups CLI flags. /// This struct regroups CLI flags.
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
pub struct ChecksumOptions { pub struct ChecksumOptions {
pub binary: bool, pub binary: bool,
pub ignore_missing: bool, pub ignore_missing: bool,
pub quiet: bool,
pub status: bool,
pub strict: bool, pub strict: bool,
pub warn: bool, pub verbose: ChecksumVerbose,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -235,20 +279,19 @@ pub fn create_sha3(bits: Option<usize>) -> UResult<HashAlgorithm> {
} }
#[allow(clippy::comparison_chain)] #[allow(clippy::comparison_chain)]
fn cksum_output(res: &ChecksumResult, status: bool) { fn print_cksum_report(res: &ChecksumResult) {
if res.bad_format == 1 { if res.bad_format == 1 {
show_warning_caps!("{} line is improperly formatted", res.bad_format); show_warning_caps!("{} line is improperly formatted", res.bad_format);
} else if res.bad_format > 1 { } else if res.bad_format > 1 {
show_warning_caps!("{} lines are improperly formatted", res.bad_format); show_warning_caps!("{} lines are improperly formatted", res.bad_format);
} }
if !status {
if res.failed_cksum == 1 { if res.failed_cksum == 1 {
show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum);
} else if res.failed_cksum > 1 { } else if res.failed_cksum > 1 {
show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum);
} }
}
if res.failed_open_file == 1 { if res.failed_open_file == 1 {
show_warning_caps!("{} listed file could not be read", res.failed_open_file); show_warning_caps!("{} listed file could not be read", res.failed_open_file);
} else if res.failed_open_file > 1 { } else if res.failed_open_file > 1 {
@ -284,10 +327,10 @@ impl FileChecksumResult {
/// The cli options might prevent to display on the outcome of the /// The cli options might prevent to display on the outcome of the
/// comparison on STDOUT. /// comparison on STDOUT.
fn can_display(&self, opts: ChecksumOptions) -> bool { fn can_display(&self, verbose: ChecksumVerbose) -> bool {
match self { match self {
FileChecksumResult::Ok => !opts.status && !opts.quiet, FileChecksumResult::Ok => verbose.over_quiet(),
FileChecksumResult::Failed => !opts.status, FileChecksumResult::Failed => verbose.over_status(),
FileChecksumResult::CantOpen => true, FileChecksumResult::CantOpen => true,
} }
} }
@ -310,9 +353,9 @@ fn print_file_report<W: Write>(
filename: &[u8], filename: &[u8],
result: FileChecksumResult, result: FileChecksumResult,
prefix: &str, prefix: &str,
opts: ChecksumOptions, verbose: ChecksumVerbose,
) { ) {
if result.can_display(opts) { if result.can_display(verbose) {
let _ = write!(w, "{prefix}"); let _ = write!(w, "{prefix}");
let _ = w.write_all(filename); let _ = w.write_all(filename);
let _ = writeln!(w, ": {result}"); let _ = writeln!(w, ": {result}");
@ -589,7 +632,7 @@ fn get_file_to_check(
filename_bytes, filename_bytes,
FileChecksumResult::CantOpen, FileChecksumResult::CantOpen,
"", "",
opts, opts.verbose,
); );
}; };
match File::open(filename) { match File::open(filename) {
@ -648,12 +691,11 @@ fn get_input_file(filename: &OsStr) -> UResult<Box<dyn Read>> {
fn identify_algo_name_and_length( fn identify_algo_name_and_length(
line_info: &LineInfo, line_info: &LineInfo,
algo_name_input: Option<&str>, algo_name_input: Option<&str>,
last_algo: &mut Option<String>,
) -> Option<(String, Option<usize>)> { ) -> Option<(String, Option<usize>)> {
let algorithm = line_info let algo_from_line = line_info.algo_name.clone().unwrap_or_default();
.algo_name let algorithm = algo_from_line.to_lowercase();
.clone() *last_algo = Some(algo_from_line);
.unwrap_or_default()
.to_lowercase();
// check if we are called with XXXsum (example: md5sum) but we detected a different algo parsing the file // check if we are called with XXXsum (example: md5sum) but we detected a different algo parsing the file
// (for example SHA1 (f) = d...) // (for example SHA1 (f) = d...)
@ -711,7 +753,7 @@ fn compute_and_check_digest_from_file(
filename, filename,
FileChecksumResult::from_bool(checksum_correct), FileChecksumResult::from_bool(checksum_correct),
prefix, prefix,
opts, opts.verbose,
); );
if checksum_correct { if checksum_correct {
@ -726,10 +768,12 @@ fn process_algo_based_line(
line_info: &LineInfo, line_info: &LineInfo,
cli_algo_name: Option<&str>, cli_algo_name: Option<&str>,
opts: ChecksumOptions, opts: ChecksumOptions,
last_algo: &mut Option<String>,
) -> Result<(), LineCheckError> { ) -> Result<(), LineCheckError> {
let filename_to_check = line_info.filename.as_slice(); let filename_to_check = line_info.filename.as_slice();
let (algo_name, algo_byte_len) = identify_algo_name_and_length(line_info, cli_algo_name) let (algo_name, algo_byte_len) =
identify_algo_name_and_length(line_info, cli_algo_name, last_algo)
.ok_or(LineCheckError::ImproperlyFormatted)?; .ok_or(LineCheckError::ImproperlyFormatted)?;
// If the digest bitlen is known, we can check the format of the expected // If the digest bitlen is known, we can check the format of the expected
@ -789,13 +833,13 @@ fn process_non_algo_based_line(
/// matched the expected. /// matched the expected.
/// If the comparison didn't happen, return a `LineChecksumError`. /// If the comparison didn't happen, return a `LineChecksumError`.
fn process_checksum_line( fn process_checksum_line(
filename_input: &OsStr,
line: &OsStr, line: &OsStr,
i: usize, i: usize,
cli_algo_name: Option<&str>, cli_algo_name: Option<&str>,
cli_algo_length: Option<usize>, cli_algo_length: Option<usize>,
opts: ChecksumOptions, opts: ChecksumOptions,
cached_regex: &mut Option<LineFormat>, cached_regex: &mut Option<LineFormat>,
last_algo: &mut Option<String>,
) -> Result<(), LineCheckError> { ) -> Result<(), LineCheckError> {
let line_bytes = os_str_as_bytes(line)?; let line_bytes = os_str_as_bytes(line)?;
@ -806,9 +850,12 @@ fn process_checksum_line(
// Use `LineInfo` to extract the data of a line. // Use `LineInfo` to extract the data of a line.
// Then, depending on its format, apply a different pre-treatment. // Then, depending on its format, apply a different pre-treatment.
if let Some(line_info) = LineInfo::parse(line, cached_regex) { let Some(line_info) = LineInfo::parse(line, cached_regex) else {
return Err(LineCheckError::ImproperlyFormatted);
};
if line_info.format == LineFormat::AlgoBased { if line_info.format == LineFormat::AlgoBased {
process_algo_based_line(&line_info, cli_algo_name, opts) process_algo_based_line(&line_info, cli_algo_name, opts, last_algo)
} else if let Some(cli_algo) = cli_algo_name { } else if let Some(cli_algo) = cli_algo_name {
// If we match a non-algo based regex, we expect a cli argument // If we match a non-algo based regex, we expect a cli argument
// to give us the algorithm to use // to give us the algorithm to use
@ -817,24 +864,6 @@ fn process_checksum_line(
// We have no clue of what algorithm to use // We have no clue of what algorithm to use
return Err(LineCheckError::ImproperlyFormatted); return Err(LineCheckError::ImproperlyFormatted);
} }
} else {
if opts.warn {
let algo = if let Some(algo_name_input) = cli_algo_name {
algo_name_input.to_uppercase()
} else {
"Unknown algorithm".to_string()
};
eprintln!(
"{}: {}: {}: improperly formatted {} checksum line",
util_name(),
&filename_input.maybe_quote(),
i + 1,
algo
);
}
Err(LineCheckError::ImproperlyFormatted)
}
} }
fn process_checksum_file( fn process_checksum_file(
@ -856,7 +885,6 @@ fn process_checksum_file(
Err(e) => { Err(e) => {
// Could not read the file, show the error and continue to the next file // Could not read the file, show the error and continue to the next file
show_error!("{e}"); show_error!("{e}");
set_exit_code(1);
return Err(FileCheckError::CantOpenChecksumFile); return Err(FileCheckError::CantOpenChecksumFile);
} }
} }
@ -868,16 +896,20 @@ fn process_checksum_file(
// cached_regex is used to ensure that several non algo-based checksum line // cached_regex is used to ensure that several non algo-based checksum line
// will use the same regex. // will use the same regex.
let mut cached_regex = None; let mut cached_regex = None;
// last_algo caches the algorithm used in the last line to print a warning
// message for the current line if improperly formatted.
// Behavior tested in gnu_cksum_c::test_warn
let mut last_algo = None;
for (i, line) in lines.iter().enumerate() { for (i, line) in lines.iter().enumerate() {
let line_result = process_checksum_line( let line_result = process_checksum_line(
filename_input,
line, line,
i, i,
cli_algo_name, cli_algo_name,
cli_algo_length, cli_algo_length,
opts, opts,
&mut cached_regex, &mut cached_regex,
&mut last_algo,
); );
// Match a first time to elude critical UErrors, and increment the total // Match a first time to elude critical UErrors, and increment the total
@ -893,7 +925,26 @@ fn process_checksum_file(
match line_result { match line_result {
Ok(()) => res.correct += 1, Ok(()) => res.correct += 1,
Err(DigestMismatch) => res.failed_cksum += 1, Err(DigestMismatch) => res.failed_cksum += 1,
Err(ImproperlyFormatted) => res.bad_format += 1, Err(ImproperlyFormatted) => {
res.bad_format += 1;
if opts.verbose.at_least_warning() {
let algo = if let Some(algo_name_input) = cli_algo_name {
Cow::Owned(algo_name_input.to_uppercase())
} else if let Some(algo) = &last_algo {
Cow::Borrowed(algo.as_str())
} else {
Cow::Borrowed("Unknown algorithm")
};
eprintln!(
"{}: {}: {}: improperly formatted {} checksum line",
util_name(),
&filename_input.maybe_quote(),
i + 1,
algo
);
}
}
Err(CantOpenFile | FileIsDirectory) => res.failed_open_file += 1, Err(CantOpenFile | FileIsDirectory) => res.failed_open_file += 1,
Err(FileNotFound) if !opts.ignore_missing => res.failed_open_file += 1, Err(FileNotFound) if !opts.ignore_missing => res.failed_open_file += 1,
_ => continue, _ => continue,
@ -903,36 +954,43 @@ fn process_checksum_file(
// not a single line correctly formatted found // not a single line correctly formatted found
// return an error // return an error
if res.total_properly_formatted() == 0 { if res.total_properly_formatted() == 0 {
if !opts.status { if opts.verbose.over_status() {
log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin));
} }
set_exit_code(1); return Err(FileCheckError::Failed);
return Err(FileCheckError::ImproperlyFormatted);
} }
// if any incorrectly formatted line, show it // if any incorrectly formatted line, show it
cksum_output(&res, opts.status); if opts.verbose.over_status() {
print_cksum_report(&res);
}
if opts.ignore_missing && res.correct == 0 { if opts.ignore_missing && res.correct == 0 {
// we have only bad format // we have only bad format
// and we had ignore-missing // and we had ignore-missing
if opts.verbose.over_status() {
eprintln!( eprintln!(
"{}: {}: no file was verified", "{}: {}: no file was verified",
util_name(), util_name(),
filename_input.maybe_quote(), filename_input.maybe_quote(),
); );
set_exit_code(1); }
return Err(FileCheckError::Failed);
} }
// strict means that we should have an exit code. // strict means that we should have an exit code.
if opts.strict && res.bad_format > 0 { if opts.strict && res.bad_format > 0 {
set_exit_code(1); return Err(FileCheckError::Failed);
} }
// if we have any failed checksum verification, we set an exit code // If a file was missing, return an error unless we explicitly ignore it.
// except if we have ignore_missing if res.failed_open_file > 0 && !opts.ignore_missing {
if (res.failed_cksum > 0 || res.failed_open_file > 0) && !opts.ignore_missing { return Err(FileCheckError::Failed);
set_exit_code(1); }
// Obviously, if a checksum failed at some point, report the error.
if res.failed_cksum > 0 {
return Err(FileCheckError::Failed);
} }
Ok(()) Ok(())
@ -950,16 +1008,23 @@ pub fn perform_checksum_validation<'a, I>(
where where
I: Iterator<Item = &'a OsStr>, I: Iterator<Item = &'a OsStr>,
{ {
let mut failed = false;
// if cksum has several input files, it will print the result for each file // if cksum has several input files, it will print the result for each file
for filename_input in files { for filename_input in files {
use FileCheckError::*; use FileCheckError::*;
match process_checksum_file(filename_input, algo_name_input, length_input, opts) { match process_checksum_file(filename_input, algo_name_input, length_input, opts) {
Err(UError(e)) => return Err(e), Err(UError(e)) => return Err(e),
Err(CantOpenChecksumFile | ImproperlyFormatted) | Ok(_) => continue, Err(Failed | CantOpenChecksumFile) => failed = true,
Ok(_) => continue,
} }
} }
if failed {
Err(USimpleError::new(1, ""))
} else {
Ok(()) Ok(())
}
} }
pub fn digest_reader<T: Read>( pub fn digest_reader<T: Read>(
@ -1416,7 +1481,7 @@ mod tests {
for (filename, result, prefix, expected) in cases { for (filename, result, prefix, expected) in cases {
let mut buffer: Vec<u8> = vec![]; let mut buffer: Vec<u8> = vec![];
print_file_report(&mut buffer, filename, *result, prefix, opts); print_file_report(&mut buffer, filename, *result, prefix, opts.verbose);
assert_eq!(&buffer, expected) assert_eq!(&buffer, expected)
} }
} }

View file

@ -0,0 +1,58 @@
// 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 chrono::{TimeZone, Utc};
use chrono_tz::{OffsetName, Tz};
use iana_time_zone::get_timezone;
/// Get the alphabetic abbreviation of the current timezone.
///
/// For example, "UTC" or "CET" or "PDT"
fn timezone_abbreviation() -> String {
let tz = match std::env::var("TZ") {
// TODO Support other time zones...
Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC,
_ => match get_timezone() {
Ok(tz_str) => tz_str.parse().unwrap(),
Err(_) => Tz::Etc__UTC,
},
};
let offset = tz.offset_from_utc_date(&Utc::now().date_naive());
offset.abbreviation().unwrap_or("UTC").to_string()
}
/// Adapt the given string to be accepted by the chrono library crate.
///
/// # Arguments
///
/// fmt: the format of the string
///
/// # Return
///
/// A string that can be used as parameter of the chrono functions that use formats
pub fn custom_time_format(fmt: &str) -> String {
// TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970
// GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`.
fmt.replace("%N", "%f")
.replace("%Z", timezone_abbreviation().as_ref())
}
#[cfg(test)]
mod tests {
use super::{custom_time_format, timezone_abbreviation};
#[test]
fn test_custom_time_format() {
assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S");
assert_eq!(custom_time_format("%d-%m-%Y %H-%M-%S"), "%d-%m-%Y %H-%M-%S");
assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S");
assert_eq!(
custom_time_format("%Y-%m-%d %H-%M-%S.%N"),
"%Y-%m-%d %H-%M-%S.%f"
);
assert_eq!(custom_time_format("%Z"), timezone_abbreviation());
}
}

View file

@ -652,14 +652,10 @@ pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool {
/// * `bool` - Returns `true` if the paths are hard links to the same file, and `false` otherwise. /// * `bool` - Returns `true` if the paths are hard links to the same file, and `false` otherwise.
#[cfg(unix)] #[cfg(unix)]
pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool { pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool {
let source_metadata = match fs::symlink_metadata(source) { let (Ok(source_metadata), Ok(target_metadata)) =
Ok(metadata) => metadata, (fs::symlink_metadata(source), fs::symlink_metadata(target))
Err(_) => return false, else {
}; return false;
let target_metadata = match fs::symlink_metadata(target) {
Ok(metadata) => metadata,
Err(_) => return false,
}; };
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
@ -682,14 +678,10 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &P
/// * `bool` - Returns `true` if either of above conditions are true, and `false` otherwise. /// * `bool` - Returns `true` if either of above conditions are true, and `false` otherwise.
#[cfg(unix)] #[cfg(unix)]
pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool { pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool {
let source_metadata = match fs::metadata(source) { let (Ok(source_metadata), Ok(target_metadata)) =
Ok(metadata) => metadata, (fs::metadata(source), fs::symlink_metadata(target))
Err(_) => return false, else {
}; return false;
let target_metadata = match fs::symlink_metadata(target) {
Ok(metadata) => metadata,
Err(_) => return false,
}; };
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()

View file

@ -79,13 +79,10 @@ pub fn apply_xattrs<P: AsRef<Path>>(
/// `true` if the file has extended attributes (indicating an ACL), `false` otherwise. /// `true` if the file has extended attributes (indicating an ACL), `false` otherwise.
pub fn has_acl<P: AsRef<Path>>(file: P) -> bool { pub fn has_acl<P: AsRef<Path>>(file: P) -> bool {
// don't use exacl here, it is doing more getxattr call then needed // don't use exacl here, it is doing more getxattr call then needed
match xattr::list(file) { xattr::list(file).is_ok_and(|acl| {
Ok(acl) => {
// if we have extra attributes, we have an acl // if we have extra attributes, we have an acl
acl.count() > 0 acl.count() > 0
} })
Err(_) => false,
}
} }
/// Returns the permissions bits of a file or directory which has Access Control List (ACL) entries based on its /// Returns the permissions bits of a file or directory which has Access Control List (ACL) entries based on its
@ -132,7 +129,7 @@ pub fn get_acl_perm_bits_from_xattr<P: AsRef<Path>>(source: P) -> u32 {
for entry in acl_entries.chunks_exact(4) { for entry in acl_entries.chunks_exact(4) {
// Third byte and fourth byte will be the perm bits // Third byte and fourth byte will be the perm bits
perm = (perm << 3) | entry[2] as u32 | entry[3] as u32; perm = (perm << 3) | u32::from(entry[2]) | u32::from(entry[3]);
} }
return perm; return perm;
} }

View file

@ -273,9 +273,7 @@ impl ChownExecutor {
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 { fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 {
let path = root.as_ref(); let path = root.as_ref();
let meta = match self.obtain_meta(path, self.dereference) { let Some(meta) = self.obtain_meta(path, self.dereference) else {
Some(m) => m,
_ => {
if self.verbosity.level == VerbosityLevel::Verbose { if self.verbosity.level == VerbosityLevel::Verbose {
println!( println!(
"failed to change ownership of {} to {}", "failed to change ownership of {} to {}",
@ -284,7 +282,6 @@ impl ChownExecutor {
); );
} }
return 1; return 1;
}
}; };
if self.recursive if self.recursive
@ -370,9 +367,8 @@ impl ChownExecutor {
Ok(entry) => entry, Ok(entry) => entry,
}; };
let path = entry.path(); let path = entry.path();
let meta = match self.obtain_meta(path, self.dereference) {
Some(m) => m, let Some(meta) = self.obtain_meta(path, self.dereference) else {
_ => {
ret = 1; ret = 1;
if entry.file_type().is_dir() { if entry.file_type().is_dir() {
// Instruct walkdir to skip this directory to avoid getting another error // Instruct walkdir to skip this directory to avoid getting another error
@ -380,7 +376,6 @@ impl ChownExecutor {
iterator.skip_current_dir(); iterator.skip_current_dir();
} }
continue; continue;
}
}; };
if self.preserve_root && is_root(path, self.traverse_symlinks == TraverseSymlinks::All) if self.preserve_root && is_root(path, self.traverse_symlinks == TraverseSymlinks::All)
@ -425,24 +420,18 @@ impl ChownExecutor {
fn obtain_meta<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> { fn obtain_meta<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> {
let path = path.as_ref(); let path = path.as_ref();
get_metadata(path, follow)
let meta = get_metadata(path, follow); .inspect_err(|e| {
if self.verbosity.level != VerbosityLevel::Silent {
match meta { show_error!(
Err(e) => {
match self.verbosity.level {
VerbosityLevel::Silent => (),
_ => show_error!(
"cannot {} {}: {}", "cannot {} {}: {}",
if follow { "dereference" } else { "access" }, if follow { "dereference" } else { "access" },
path.quote(), path.quote(),
strip_errno(&e) strip_errno(e)
), );
}
None
}
Ok(meta) => Some(meta),
} }
})
.ok()
} }
#[inline] #[inline]

View file

@ -46,6 +46,8 @@ pub use crate::features::buf_copy;
pub use crate::features::checksum; pub use crate::features::checksum;
#[cfg(feature = "colors")] #[cfg(feature = "colors")]
pub use crate::features::colors; pub use crate::features::colors;
#[cfg(feature = "custom-tz-fmt")]
pub use crate::features::custom_tz_fmt;
#[cfg(feature = "encoding")] #[cfg(feature = "encoding")]
pub use crate::features::encoding; pub use crate::features::encoding;
#[cfg(feature = "format")] #[cfg(feature = "format")]

View file

@ -269,16 +269,9 @@ impl<'parser> Parser<'parser> {
/// Same as `parse()` but tries to return u64 /// Same as `parse()` but tries to return u64
pub fn parse_u64(&self, size: &str) -> Result<u64, ParseSizeError> { pub fn parse_u64(&self, size: &str) -> Result<u64, ParseSizeError> {
match self.parse(size) { self.parse(size).and_then(|num_u128| {
Ok(num_u128) => { u64::try_from(num_u128).map_err(|_| ParseSizeError::size_too_big(size))
let num_u64 = match u64::try_from(num_u128) { })
Ok(n) => n,
Err(_) => return Err(ParseSizeError::size_too_big(size)),
};
Ok(num_u64)
}
Err(e) => Err(e),
}
} }
/// Same as `parse_u64()`, except returns `u64::MAX` on overflow /// Same as `parse_u64()`, except returns `u64::MAX` on overflow

View file

@ -49,9 +49,8 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
if len == 0 { if len == 0 {
return Err("empty string".to_owned()); return Err("empty string".to_owned());
} }
let slice = match string.get(..len - 1) { let Some(slice) = string.get(..len - 1) else {
Some(s) => s, return Err(format!("invalid time interval {}", string.quote()));
None => return Err(format!("invalid time interval {}", string.quote())),
}; };
let (numstr, times) = match string.chars().next_back().unwrap() { let (numstr, times) = match string.chars().next_back().unwrap() {
's' => (slice, 1), 's' => (slice, 1),

View file

@ -2,7 +2,7 @@
// //
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb // spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont
use crate::common::util::TestScenario; use crate::common::util::TestScenario;
@ -1284,6 +1284,18 @@ fn test_several_files_error_mgmt() {
.stderr_contains("incorrect: no properly "); .stderr_contains("incorrect: no properly ");
} }
#[test]
fn test_check_unknown_checksum_file() {
let scene = TestScenario::new(util_name!());
scene
.ucmd()
.arg("--check")
.arg("missing")
.fails()
.stderr_only("cksum: missing: No such file or directory\n");
}
#[test] #[test]
fn test_check_comment_line() { fn test_check_comment_line() {
// A comment in a checksum file shall be discarded unnoticed. // A comment in a checksum file shall be discarded unnoticed.
@ -1811,6 +1823,373 @@ mod gnu_cksum_base64 {
} }
} }
/// This module reimplements the cksum-c.sh GNU test.
mod gnu_cksum_c {
use super::*;
const INVALID_SUM: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaafdb57c725157cb40b5aee8d937b8351477e";
fn make_scene() -> TestScenario {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.write("input", "9\n7\n1\n4\n2\n6\n3\n5\n8\n10\n");
let algos: &[&[&str]] = &[
&["-a", "sha384"],
&["-a", "blake2b"],
&["-a", "blake2b", "-l", "384"],
&["-a", "sm3"],
];
for args in algos {
let result = scene.ucmd().args(args).succeeds();
let stdout = result.stdout();
at.append_bytes("CHECKSUMS", stdout);
}
scene
}
#[test]
#[ignore]
fn test_signed_checksums() {
todo!()
}
#[test]
fn test_check_individual_digests_in_mixed_file() {
let scene = make_scene();
scene
.ucmd()
.arg("--check")
.arg("-a")
.arg("sm3")
.arg("CHECKSUMS")
.succeeds();
}
#[test]
fn test_check_against_older_non_hex_formats() {
let scene = make_scene();
scene
.ucmd()
.arg("-c")
.arg("-a")
.arg("crc")
.arg("CHECKSUMS")
.fails();
let crc_cmd = scene.ucmd().arg("-a").arg("crc").arg("input").succeeds();
let crc_cmd_out = crc_cmd.stdout();
scene.fixtures.write_bytes("CHECKSUMS.crc", crc_cmd_out);
scene.ucmd().arg("-c").arg("CHECKSUMS.crc").fails();
}
#[test]
fn test_status() {
let scene = make_scene();
scene
.ucmd()
.arg("--status")
.arg("--check")
.arg("CHECKSUMS")
.succeeds()
.no_output();
}
fn make_scene_with_comment() -> TestScenario {
let scene = make_scene();
scene
.fixtures
.append("CHECKSUMS", "# Very important comment\n");
scene
}
#[test]
fn test_status_with_comment() {
let scene = make_scene_with_comment();
scene
.ucmd()
.arg("--status")
.arg("--check")
.arg("CHECKSUMS")
.succeeds()
.no_output();
}
fn make_scene_with_invalid_line() -> TestScenario {
let scene = make_scene_with_comment();
scene.fixtures.append("CHECKSUMS", "invalid_line\n");
scene
}
#[test]
fn test_check_strict() {
let scene = make_scene_with_invalid_line();
// without strict, succeeds
scene
.ucmd()
.arg("--check")
.arg("CHECKSUMS")
.succeeds()
.stderr_contains("1 line is improperly formatted");
// with strict, fails
scene
.ucmd()
.arg("--strict")
.arg("--check")
.arg("CHECKSUMS")
.fails()
.stderr_contains("1 line is improperly formatted");
}
fn make_scene_with_two_invalid_lines() -> TestScenario {
let scene = make_scene_with_comment();
scene
.fixtures
.append("CHECKSUMS", "invalid_line\ninvalid_line\n");
scene
}
#[test]
fn test_check_strict_plural_checks() {
let scene = make_scene_with_two_invalid_lines();
scene
.ucmd()
.arg("--strict")
.arg("--check")
.arg("CHECKSUMS")
.fails()
.stderr_contains("2 lines are improperly formatted");
}
fn make_scene_with_incorrect_checksum() -> TestScenario {
let scene = make_scene_with_two_invalid_lines();
scene
.fixtures
.append("CHECKSUMS", &format!("SM3 (input) = {INVALID_SUM}\n"));
scene
}
#[test]
fn test_check_with_incorrect_checksum() {
let scene = make_scene_with_incorrect_checksum();
scene
.ucmd()
.arg("--check")
.arg("CHECKSUMS")
.fails()
.stdout_contains("input: FAILED")
.stderr_contains("1 computed checksum did NOT match");
// also fails with strict
scene
.ucmd()
.arg("--strict")
.arg("--check")
.arg("CHECKSUMS")
.fails()
.stdout_contains("input: FAILED")
.stderr_contains("1 computed checksum did NOT match");
}
#[test]
fn test_status_with_errors() {
let scene = make_scene_with_incorrect_checksum();
scene
.ucmd()
.arg("--status")
.arg("--check")
.arg("CHECKSUMS")
.fails()
.no_output();
}
#[test]
fn test_check_with_non_existing_file() {
let scene = make_scene();
scene
.fixtures
.write("CHECKSUMS2", &format!("SM3 (input2) = {INVALID_SUM}\n"));
scene
.ucmd()
.arg("--check")
.arg("CHECKSUMS2")
.fails()
.stdout_contains("input2: FAILED open or read")
.stderr_contains("1 listed file could not be read");
// also fails with strict
scene
.ucmd()
.arg("--strict")
.arg("--check")
.arg("CHECKSUMS2")
.fails()
.stdout_contains("input2: FAILED open or read")
.stderr_contains("1 listed file could not be read");
}
fn make_scene_with_another_improperly_formatted() -> TestScenario {
let scene = make_scene_with_incorrect_checksum();
scene.fixtures.append(
"CHECKSUMS",
&format!("BLAKE2b (missing-file) = {INVALID_SUM}\n"),
);
scene
}
#[test]
fn test_warn() {
let scene = make_scene_with_another_improperly_formatted();
scene
.ucmd()
.arg("--warn")
.arg("--check")
.arg("CHECKSUMS")
.run()
.stderr_contains("CHECKSUMS: 6: improperly formatted SM3 checksum line")
.stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line");
}
fn make_scene_with_checksum_missing() -> TestScenario {
let scene = make_scene_with_another_improperly_formatted();
scene.fixtures.write(
"CHECKSUMS-missing",
&format!("SM3 (nonexistent) = {INVALID_SUM}\n"),
);
scene
}
#[test]
fn test_ignore_missing() {
let scene = make_scene_with_checksum_missing();
scene
.ucmd()
.arg("--ignore-missing")
.arg("--check")
.arg("CHECKSUMS-missing")
.fails()
.stdout_does_not_contain("nonexistent: No such file or directory")
.stdout_does_not_contain("nonexistent: FAILED open or read")
.stderr_contains("CHECKSUMS-missing: no file was verified");
}
#[test]
fn test_status_and_warn() {
let scene = make_scene_with_checksum_missing();
// --status before --warn
scene
.ucmd()
.arg("--status")
.arg("--warn")
.arg("--check")
.arg("CHECKSUMS")
.fails()
.stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line")
.stderr_contains("WARNING: 3 lines are improperly formatted")
.stderr_contains("WARNING: 1 computed checksum did NOT match");
// --warn before --status (status hides the results)
scene
.ucmd()
.arg("--warn")
.arg("--status")
.arg("--check")
.arg("CHECKSUMS")
.fails()
.stderr_does_not_contain("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line")
.stderr_does_not_contain("WARNING: 3 lines are improperly formatted")
.stderr_does_not_contain("WARNING: 1 computed checksum did NOT match");
}
#[test]
fn test_status_and_ignore_missing() {
let scene = make_scene_with_checksum_missing();
scene
.ucmd()
.arg("--status")
.arg("--ignore-missing")
.arg("--check")
.arg("CHECKSUMS")
.fails()
.no_output();
}
#[test]
fn test_status_warn_and_ignore_missing() {
let scene = make_scene_with_checksum_missing();
scene
.ucmd()
.arg("--status")
.arg("--warn")
.arg("--ignore-missing")
.arg("--check")
.arg("CHECKSUMS-missing")
.fails()
.stderr_contains("CHECKSUMS-missing: no file was verified")
.stdout_does_not_contain("nonexistent: No such file or directory");
}
#[test]
fn test_check_several_files_dont_exist() {
let scene = make_scene();
scene
.ucmd()
.arg("--check")
.arg("non-existing-1")
.arg("non-existing-2")
.fails()
.stderr_contains("non-existing-1: No such file or directory")
.stderr_contains("non-existing-2: No such file or directory");
}
#[test]
fn test_check_several_files_empty() {
let scene = make_scene();
scene.fixtures.touch("empty-1");
scene.fixtures.touch("empty-2");
scene
.ucmd()
.arg("--check")
.arg("empty-1")
.arg("empty-2")
.fails()
.stderr_contains("empty-1: no properly formatted checksum lines found")
.stderr_contains("empty-2: no properly formatted checksum lines found");
}
}
/// The tests in this module check the behavior of cksum when given different /// The tests in this module check the behavior of cksum when given different
/// checksum formats and algorithms in the same file, while specifying an /// checksum formats and algorithms in the same file, while specifying an
/// algorithm on CLI or not. /// algorithm on CLI or not.

View file

@ -950,7 +950,7 @@ mod tests_split_iterator {
| '*' | '?' | '[' | '#' | '˜' | '=' | '%' => { | '*' | '?' | '[' | '#' | '˜' | '=' | '%' => {
special = true; special = true;
} }
_ => continue, _ => (),
} }
} }

View file

@ -63,7 +63,7 @@ fn test_kill_list_all_signals() {
.stdout_contains("KILL") .stdout_contains("KILL")
.stdout_contains("TERM") .stdout_contains("TERM")
.stdout_contains("HUP") .stdout_contains("HUP")
.stdout_does_not_contain("EXIT"); .stdout_contains("EXIT");
} }
#[test] #[test]
@ -80,15 +80,16 @@ fn test_kill_list_all_signals_as_table() {
.succeeds() .succeeds()
.stdout_contains("KILL") .stdout_contains("KILL")
.stdout_contains("TERM") .stdout_contains("TERM")
.stdout_contains("HUP"); .stdout_contains("HUP")
.stdout_contains("EXIT");
} }
#[test] #[test]
fn test_kill_table_starts_at_1() { fn test_kill_table_starts_at_0() {
new_ucmd!() new_ucmd!()
.arg("-t") .arg("-t")
.succeeds() .succeeds()
.stdout_matches(&Regex::new("^\\s?1\\sHUP").unwrap()); .stdout_matches(&Regex::new("^\\s?0\\sEXIT").unwrap());
} }
#[test] #[test]
@ -104,6 +105,7 @@ fn test_kill_table_lists_all_vertically() {
assert!(signals.contains(&"KILL")); assert!(signals.contains(&"KILL"));
assert!(signals.contains(&"TERM")); assert!(signals.contains(&"TERM"));
assert!(signals.contains(&"HUP")); assert!(signals.contains(&"HUP"));
assert!(signals.contains(&"EXIT"));
} }
#[test] #[test]
@ -143,6 +145,7 @@ fn test_kill_list_all_vertically() {
assert!(signals.contains(&"KILL")); assert!(signals.contains(&"KILL"));
assert!(signals.contains(&"TERM")); assert!(signals.contains(&"TERM"));
assert!(signals.contains(&"HUP")); assert!(signals.contains(&"HUP"));
assert!(signals.contains(&"EXIT"));
} }
#[test] #[test]

View file

@ -1315,3 +1315,30 @@ fn test_same_sort_mode_twice() {
fn test_args_override() { fn test_args_override() {
new_ucmd!().args(&["-f", "-f"]).pipe_in("foo").succeeds(); new_ucmd!().args(&["-f", "-f"]).pipe_in("foo").succeeds();
} }
#[test]
fn test_k_overflow() {
let input = "2\n1\n";
let output = "1\n2\n";
new_ucmd!()
.args(&["-k", "18446744073709551616"])
.pipe_in(input)
.succeeds()
.stdout_is(output);
}
#[test]
fn test_human_blocks_r_and_q() {
let input = "1Q\n1R\n";
let output = "1R\n1Q\n";
new_ucmd!()
.args(&["-h"])
.pipe_in(input)
.succeeds()
.stdout_is(output);
}
#[test]
fn test_args_check_conflict() {
new_ucmd!().arg("-c").arg("-C").fails();
}

View file

@ -65,13 +65,11 @@ fn test_zero_timeout() {
new_ucmd!() new_ucmd!()
.args(&["-v", "0", "sleep", ".1"]) .args(&["-v", "0", "sleep", ".1"])
.succeeds() .succeeds()
.no_stderr() .no_output();
.no_stdout();
new_ucmd!() new_ucmd!()
.args(&["-v", "0", "-s0", "-k0", "sleep", ".1"]) .args(&["-v", "0", "-s0", "-k0", "sleep", ".1"])
.succeeds() .succeeds()
.no_stderr() .no_output();
.no_stdout();
} }
#[test] #[test]
@ -83,14 +81,26 @@ fn test_command_empty_args() {
} }
#[test] #[test]
fn test_preserve_status() { fn test_foreground() {
for arg in ["-f", "--foreground"] {
new_ucmd!() new_ucmd!()
.args(&["--preserve-status", ".1", "sleep", "10"]) .args(&[arg, ".1", "sleep", "10"])
.fails()
.code_is(124)
.no_output();
}
}
#[test]
fn test_preserve_status() {
for arg in ["-p", "--preserve-status"] {
new_ucmd!()
.args(&[arg, ".1", "sleep", "10"])
.fails() .fails()
// 128 + SIGTERM = 128 + 15 // 128 + SIGTERM = 128 + 15
.code_is(128 + 15) .code_is(128 + 15)
.no_stderr() .no_output();
.no_stdout(); }
} }
#[test] #[test]
@ -102,8 +112,7 @@ fn test_preserve_status_even_when_send_signal() {
.args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "2"]) .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "2"])
.succeeds() .succeeds()
.code_is(0) .code_is(0)
.no_stderr() .no_output();
.no_stdout();
} }
} }
@ -113,14 +122,12 @@ fn test_dont_overflow() {
.args(&["9223372036854775808d", "sleep", "0"]) .args(&["9223372036854775808d", "sleep", "0"])
.succeeds() .succeeds()
.code_is(0) .code_is(0)
.no_stderr() .no_output();
.no_stdout();
new_ucmd!() new_ucmd!()
.args(&["-k", "9223372036854775808d", "10", "sleep", "0"]) .args(&["-k", "9223372036854775808d", "10", "sleep", "0"])
.succeeds() .succeeds()
.code_is(0) .code_is(0)
.no_stderr() .no_output();
.no_stdout();
} }
#[test] #[test]
@ -153,8 +160,7 @@ fn test_kill_after_long() {
new_ucmd!() new_ucmd!()
.args(&["--kill-after=1", "1", "sleep", "0"]) .args(&["--kill-after=1", "1", "sleep", "0"])
.succeeds() .succeeds()
.no_stdout() .no_output();
.no_stderr();
} }
#[test] #[test]

View file

@ -94,6 +94,12 @@ if [ "$(uname)" == "Linux" ]; then
export SELINUX_ENABLED=1 export SELINUX_ENABLED=1
fi fi
# Set up quilt for patch management
export QUILT_PATCHES="${ME_dir}/gnu-patches/"
cd "$path_GNU"
quilt push -a
cd -
"${MAKE}" PROFILE="${UU_MAKE_PROFILE}" "${MAKE}" PROFILE="${UU_MAKE_PROFILE}"
cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target
@ -206,8 +212,6 @@ grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r sed -Ei
# we should not regress our project just to match what GNU is going. # we should not regress our project just to match what GNU is going.
# So, do some changes on the fly # So, do some changes on the fly
eval cat "$path_UUTILS/util/gnu-patches/*.patch" | patch -N -r - -d "$path_GNU" -p 1 -i - || true
sed -i -e "s|rm: cannot remove 'e/slink'|rm: cannot remove 'e'|g" tests/rm/fail-eacces.sh sed -i -e "s|rm: cannot remove 'e/slink'|rm: cannot remove 'e'|g" tests/rm/fail-eacces.sh
sed -i -e "s|rm: cannot remove 'a/b/file'|rm: cannot remove 'a'|g" tests/rm/cycle.sh sed -i -e "s|rm: cannot remove 'a/b/file'|rm: cannot remove 'a'|g" tests/rm/cycle.sh

10
util/gnu-patches/series Normal file
View file

@ -0,0 +1,10 @@
tests_factor_factor.pl.patch
tests_cksum_base64.patch
tests_comm.pl.patch
tests_cut_error_msg.patch
tests_dup_source.patch
tests_env_env-S.pl.patch
tests_invalid_opt.patch
tests_ls_no_cap.patch
tests_sort_merge.pl.patch
tests_tsort.patch

View file

@ -1,8 +1,8 @@
diff --git a/tests/cksum/cksum-base64.pl b/tests/cksum/cksum-base64.pl Index: gnu/tests/cksum/cksum-base64.pl
index a037a1628..c6d87d447 100755 ===================================================================
--- a/tests/cksum/cksum-base64.pl --- gnu.orig/tests/cksum/cksum-base64.pl
+++ b/tests/cksum/cksum-base64.pl +++ gnu/tests/cksum/cksum-base64.pl
@@ -91,8 +91,8 @@ my $prog = 'cksum'; @@ -92,8 +92,8 @@ my $prog = 'cksum';
my $fail = run_tests ($program_name, $prog, \@Tests, $save_temps, $verbose); my $fail = run_tests ($program_name, $prog, \@Tests, $save_temps, $verbose);
# Ensure hash names from cksum --help match those in @pairs above. # Ensure hash names from cksum --help match those in @pairs above.

View file

@ -1,7 +1,7 @@
diff --git a/tests/misc/comm.pl b/tests/misc/comm.pl Index: gnu/tests/misc/comm.pl
index 5bd5f56d7..8322d92ba 100755 ===================================================================
--- a/tests/misc/comm.pl --- gnu.orig/tests/misc/comm.pl
+++ b/tests/misc/comm.pl +++ gnu/tests/misc/comm.pl
@@ -73,18 +73,24 @@ my @Tests = @@ -73,18 +73,24 @@ my @Tests =
# invalid missing command line argument (1) # invalid missing command line argument (1)

View file

@ -1,7 +1,7 @@
diff --git a/tests/cut/cut.pl b/tests/cut/cut.pl Index: gnu/tests/cut/cut.pl
index 1670db02e..ed633792a 100755 ===================================================================
--- a/tests/cut/cut.pl --- gnu.orig/tests/cut/cut.pl
+++ b/tests/cut/cut.pl +++ gnu/tests/cut/cut.pl
@@ -29,13 +29,15 @@ my $mb_locale = $ENV{LOCALE_FR_UTF8}; @@ -29,13 +29,15 @@ my $mb_locale = $ENV{LOCALE_FR_UTF8};
my $prog = 'cut'; my $prog = 'cut';

View file

@ -1,8 +1,8 @@
diff --git a/tests/mv/dup-source.sh b/tests/mv/dup-source.sh Index: gnu/tests/mv/dup-source.sh
index 7bcd82fc3..0f9005296 100755 ===================================================================
--- a/tests/mv/dup-source.sh --- gnu.orig/tests/mv/dup-source.sh
+++ b/tests/mv/dup-source.sh +++ gnu/tests/mv/dup-source.sh
@@ -83,7 +83,7 @@ $i: cannot stat 'a': No such file or directory @@ -83,7 +83,7 @@ $i: cannot stat 'a': No such file or dir
$i: cannot stat 'a': No such file or directory $i: cannot stat 'a': No such file or directory
$i: cannot stat 'b': No such file or directory $i: cannot stat 'b': No such file or directory
$i: cannot move './b' to a subdirectory of itself, 'b/b' $i: cannot move './b' to a subdirectory of itself, 'b/b'

View file

@ -1,7 +1,7 @@
diff --git a/tests/env/env-S.pl b/tests/env/env-S.pl Index: gnu/tests/env/env-S.pl
index 710ca82cf..af7cf6efa 100755 ===================================================================
--- a/tests/env/env-S.pl --- gnu.orig/tests/env/env-S.pl
+++ b/tests/env/env-S.pl +++ gnu/tests/env/env-S.pl
@@ -209,27 +209,28 @@ my @Tests = @@ -209,27 +209,28 @@ my @Tests =
{ERR=>"$prog: no terminating quote in -S string\n"}], {ERR=>"$prog: no terminating quote in -S string\n"}],
['err5', q[-S'A=B\\q'], {EXIT=>125}, ['err5', q[-S'A=B\\q'], {EXIT=>125},

View file

@ -1,7 +1,7 @@
diff --git a/tests/factor/factor.pl b/tests/factor/factor.pl Index: gnu/tests/factor/factor.pl
index b1406c266..3d97cd6a5 100755 ===================================================================
--- a/tests/factor/factor.pl --- gnu.orig/tests/factor/factor.pl
+++ b/tests/factor/factor.pl +++ gnu/tests/factor/factor.pl
@@ -61,12 +61,14 @@ my @Tests = @@ -61,12 +61,14 @@ my @Tests =
# Map newer glibc diagnostic to expected. # Map newer glibc diagnostic to expected.
# Also map OpenBSD 5.1's "unknown option" to expected "invalid option". # Also map OpenBSD 5.1's "unknown option" to expected "invalid option".

View file

@ -1,7 +1,7 @@
diff --git a/tests/misc/invalid-opt.pl b/tests/misc/invalid-opt.pl Index: gnu/tests/misc/invalid-opt.pl
index 4b9c4c184..4ccd89482 100755 ===================================================================
--- a/tests/misc/invalid-opt.pl --- gnu.orig/tests/misc/invalid-opt.pl
+++ b/tests/misc/invalid-opt.pl +++ gnu/tests/misc/invalid-opt.pl
@@ -74,23 +74,13 @@ foreach my $prog (@built_programs) @@ -74,23 +74,13 @@ foreach my $prog (@built_programs)
defined $out defined $out
or $out = ''; or $out = '';

View file

@ -1,7 +1,7 @@
diff --git a/tests/sort/sort-merge.pl b/tests/sort/sort-merge.pl Index: gnu/tests/sort/sort-merge.pl
index 89eed0c64..c2f5aa7e5 100755 ===================================================================
--- a/tests/sort/sort-merge.pl --- gnu.orig/tests/sort/sort-merge.pl
+++ b/tests/sort/sort-merge.pl +++ gnu/tests/sort/sort-merge.pl
@@ -43,22 +43,22 @@ my @Tests = @@ -43,22 +43,22 @@ my @Tests =
# check validation of --batch-size option # check validation of --batch-size option
['nmerge-0', "-m --batch-size=0", @inputs, ['nmerge-0', "-m --batch-size=0", @inputs,

View file

@ -1,7 +1,7 @@
diff --git a/tests/misc/tsort.pl b/tests/misc/tsort.pl Index: gnu/tests/misc/tsort.pl
index 70bdc474c..4fd420a4e 100755 ===================================================================
--- a/tests/misc/tsort.pl --- gnu.orig/tests/misc/tsort.pl
+++ b/tests/misc/tsort.pl +++ gnu/tests/misc/tsort.pl
@@ -54,8 +54,10 @@ my @Tests = @@ -54,8 +54,10 @@ my @Tests =
['only-one', {IN => {f => ""}}, {IN => {g => ""}}, ['only-one', {IN => {f => ""}}, {IN => {g => ""}},