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/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/env/locales/en-US.ftl b/src/uu/env/locales/en-US.ftl index 60f9261816e..c87b2cf2fca 100644 --- a/src/uu/env/locales/en-US.ftl +++ b/src/uu/env/locales/en-US.ftl @@ -6,6 +6,7 @@ env-after-help = A mere - implies -i. If no COMMAND, print the resulting environ env-help-ignore-environment = start with an empty environment env-help-chdir = change working directory to DIR env-help-null = end each output line with a 0 byte rather than a newline (only valid when printing the environment) +env-help-env0-from = read NUL-terminated environment entries from FILE, or standard input if FILE is '-' env-help-unset = remove variable from the environment env-help-debug = print verbose information for each processing step env-help-split-string = process and split S into separate arguments; used to pass multiple arguments on shebang lines @@ -33,6 +34,7 @@ env-error-use-s-shebang = use -[v]S to pass options in shebang lines env-error-cannot-unset = cannot unset '{ $name }': Invalid argument env-error-cannot-unset-invalid = cannot unset { $name }: Invalid argument env-error-must-specify-command-with-chdir = must specify command with --chdir (-C) +env-error-env0-from-not-nul-terminated = { $file }: file must be empty or end with a NUL byte env-error-cannot-change-directory = cannot change directory to { $directory }: { $error } env-error-argv0-not-supported = --argv0 is currently not supported on this platform env-error-failed-set-signal-action = failed to set signal action for signal { $signal }: { $error } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 02505edbed5..5405c067bc5 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -20,6 +20,11 @@ use native_int_str::{ }; #[cfg(all(unix, not(target_os = "fuchsia")))] use nix::libc; +// libc does not expose `environ` on every target, so declare it ourselves. +#[cfg(all(unix, not(target_os = "fuchsia")))] +unsafe extern "C" { + static mut environ: *mut *mut libc::c_char; +} #[cfg(all(unix, not(target_os = "fuchsia")))] use nix::sys::signal::{SigSet, SigmaskHow, Signal, sigprocmask}; #[cfg(unix)] @@ -33,16 +38,18 @@ use std::env; #[cfg(unix)] use std::ffi::CString; use std::ffi::{OsStr, OsString}; -#[cfg(not(unix))] +use std::fmt; +use std::fs; use std::io; +use std::io::Read as _; use std::io::Write as _; use std::io::stderr; #[cfg(all(unix, not(target_os = "fuchsia")))] use std::mem::zeroed; #[cfg(unix)] -use std::os::unix::ffi::OsStrExt; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; -use uucore::display::{Quotable, print_all_env_vars}; +use uucore::display::{OsWrite, Quotable, print_all_env_vars}; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, strip_errno}; use uucore::line_ending::LineEnding; #[cfg(all(unix, not(target_os = "fuchsia")))] @@ -91,6 +98,7 @@ mod options { pub const IGNORE_ENVIRONMENT: &str = "ignore-environment"; pub const CHDIR: &str = "chdir"; pub const NULL: &str = "null"; + pub const ENV0_FROM: &str = "env0-from"; pub const UNSET: &str = "unset"; pub const DEBUG: &str = "debug"; pub const SPLIT_STRING: &str = "split-string"; @@ -105,6 +113,7 @@ struct Options<'a> { ignore_env: bool, line_ending: LineEnding, running_directory: Option<&'a OsStr>, + env0_from: Vec<&'a OsStr>, unsets: Vec<&'a OsStr>, sets: Vec<(Cow<'a, OsStr>, Cow<'a, OsStr>)>, program: Vec<&'a OsStr>, @@ -311,6 +320,198 @@ fn signal_is_valid(sig: usize) -> bool { true } +/// One environment entry, kept verbatim as `NAME=VALUE` (or an entry without +/// a '=' byte). An explicit model is needed for `--env0-from`, which must +/// preserve entries exactly: their order, duplicate names, empty entries and +/// entries without a '=' byte. +type EnvEntries = Vec; + +fn env_join(name: &OsStr, value: &OsStr) -> OsString { + #[cfg(unix)] + { + let mut bytes = name.as_bytes().to_vec(); + bytes.push(b'='); + bytes.extend_from_slice(value.as_bytes()); + OsString::from_vec(bytes) + } + #[cfg(not(unix))] + { + let mut joined = name.to_os_string(); + joined.push("="); + joined.push(value); + joined + } +} + +/// Split an entry at its first '=' byte; `None` for entries without one. +fn split_env_entry(entry: &OsStr) -> Option<(&OsStr, &OsStr)> { + #[cfg(unix)] + { + let bytes = entry.as_bytes(); + let pos = bytes.iter().position(|&b| b == b'=')?; + Some(( + OsStr::from_bytes(&bytes[..pos]), + OsStr::from_bytes(&bytes[pos + 1..]), + )) + } + #[cfg(not(unix))] + { + let text = entry.to_str()?; + let (name, value) = text.split_once('=')?; + Some((OsStr::new(name), OsStr::new(value))) + } +} + +fn entry_name(entry: &OsStr) -> Option<&OsStr> { + split_env_entry(entry).map(|(name, _)| name) +} + +/// Set `name` to `value`, replacing the first entry with that name (like +/// `setenv`), or appending a new entry when there is none. +fn set_env_entry(entries: &mut EnvEntries, name: &OsStr, value: &OsStr) { + let new_entry = env_join(name, value); + if let Some(i) = entries.iter().position(|e| entry_name(e) == Some(name)) { + entries[i] = new_entry; + } else { + entries.push(new_entry); + } +} + +fn inherited_env_entries() -> EnvEntries { + env::vars_os() + .map(|(name, value)| env_join(&name, &value)) + .collect() +} + +fn env_entry_from_bytes(bytes: &[u8]) -> OsString { + #[cfg(unix)] + { + OsString::from_vec(bytes.to_vec()) + } + #[cfg(not(unix))] + { + OsString::from(String::from_utf8_lossy(bytes).into_owned()) + } +} + +/// Read NUL-terminated environment entries from `file`, or standard input when +/// `file` is '-'. The content must either be empty or end with a NUL byte. +fn read_env0_entries(file: &OsStr) -> UResult { + let mut data = Vec::new(); + if file == "-" { + io::stdin() + .lock() + .read_to_end(&mut data) + .map_err(|e| USimpleError::new(1, format!("{}: {e}", file.maybe_quote())))?; + } else { + data = fs::read(file).map_err(|e| { + USimpleError::new(1, format!("{}: {}", file.maybe_quote(), strip_errno(&e))) + })?; + } + + if !data.is_empty() && *data.last().unwrap() != 0 { + return Err(USimpleError::new( + 1, + translate!("env-error-env0-from-not-nul-terminated", "file" => file.quote()), + )); + } + + if data.is_empty() { + return Ok(EnvEntries::new()); + } + + // The split ends in the NUL terminator itself, which is not an entry. The + // chunks in between, including empty ones between two NUL bytes, are + // preserved verbatim. + let mut entries: EnvEntries = data.split(|&b| b == 0).map(env_entry_from_bytes).collect(); + entries.pop(); + Ok(entries) +} + +/// Merge `--env0-from` entries into the inherited environment: entries of the +/// form `name=value` replace the variable with the same name (a later +/// assignment wins); entries without a '=' byte, including empty ones, are +/// appended without replacing anything. +fn merge_env0_entries(entries: &mut EnvEntries, file_entries: EnvEntries) { + for entry in file_entries { + if let Some((name, value)) = split_env_entry(&entry) { + set_env_entry(entries, name, value); + } else { + entries.push(entry); + } + } +} + +/// Build the environment model for `--env0-from`. With `--ignore-environment` +/// the file entries are the environment itself, so their order and contents +/// are preserved exactly (including duplicates and entries without '='); +/// `--unset` and `NAME=VALUE` operands then apply on top. +/// Multiple `--env0-from` files are processed in the order given. +fn build_env_model(opts: &Options<'_>) -> UResult { + let mut entries = if opts.ignore_env { + EnvEntries::new() + } else { + inherited_env_entries() + }; + + for file in &opts.env0_from { + let file_entries = read_env0_entries(file)?; + if entries.is_empty() { + entries = file_entries; + } else { + merge_env0_entries(&mut entries, file_entries); + } + } + + for name in &opts.unsets { + validate_unset_name(name)?; + let name = *name; + entries.retain(|entry| entry_name(entry) != Some(name)); + } + + for (name, value) in &opts.sets { + if name.is_empty() { + show_warning!( + "{}", + translate!("env-warning-no-name-specified", "value" => value.quote()) + ); + continue; + } + set_env_entry(&mut entries, name, value); + } + + Ok(entries) +} + +/// Print the environment model verbatim, one entry per line ending. +fn print_env_model(entries: &EnvEntries, line_ending: T) -> io::Result<()> { + let mut stdout = io::stdout().lock(); + for entry in entries { + stdout.write_all_os(entry)?; + write!(stdout, "{line_ending}")?; + } + Ok(()) +} + +/// Materialize a model into the process environment. Only used on platforms +/// where the exec'd environment cannot be passed explicitly (non-Unix), where +/// entries without a '=' byte cannot be represented anyway. +#[cfg(not(all(unix, not(target_os = "fuchsia"))))] +fn materialize_env_model(entries: &[OsString]) { + for (name, _) in env::vars_os() { + unsafe { + env::remove_var(name); + } + } + for entry in entries { + if let Some((name, value)) = split_env_entry(entry) { + unsafe { + env::set_var(name, value); + } + } + } +} + pub fn uu_app() -> Command { Command::new("env") .version(uucore::crate_version!()) @@ -344,6 +545,15 @@ pub fn uu_app() -> Command { .help(translate!("env-help-null")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::ENV0_FROM) + .long(options::ENV0_FROM) + .value_name("PATH") + .value_hint(clap::ValueHint::FilePath) + .value_parser(ValueParser::os_string()) + .action(ArgAction::Append) + .help(translate!("env-help-env0-from")), + ) .arg( Arg::new(options::UNSET) .short('u') @@ -576,7 +786,12 @@ impl EnvAppData { let mut process_flags = true; let mut expecting_arg = false; // Leave out split-string since it's a special case below - let flags_with_args = [options::ARGV0, options::CHDIR, options::UNSET]; + let flags_with_args = [ + options::ARGV0, + options::CHDIR, + options::ENV0_FROM, + options::UNSET, + ]; let short_flags_with_args = ['a', 'C', 'u']; let mut consumed_split_payload_arg: Option = None; for (n, arg) in original_args.iter().enumerate() { @@ -785,11 +1000,22 @@ impl EnvAppData { // NOTE: we manually set and unset the env vars below rather than using Command::env() to more // easily handle the case where no command is given - apply_removal_of_all_env_vars(&opts); + // With --env0-from the environment is carried in an explicit model so + // entries can be preserved verbatim (order, duplicates, entries + // without a '=' byte), mirroring GNU. Without it the inherited + // environment is modified in place, as before. + #[allow(unused_mut)] + let mut env_model = if opts.env0_from.is_empty() { + apply_removal_of_all_env_vars(&opts); - apply_unset_env_vars(&opts)?; + apply_unset_env_vars(&opts)?; - apply_specified_env_vars(&opts); + apply_specified_env_vars(&opts); + + None + } else { + Some(build_env_model(&opts)?) + }; #[cfg(all(unix, not(target_os = "fuchsia")))] { @@ -815,14 +1041,32 @@ impl EnvAppData { if opts.list_signal_handling { list_signal_handling(&signal_action_log); } + + // The exec environment is taken from the model, so also carry over + // RUST_SIGPIPE there (it is otherwise only set in the process + // environment by apply_signal_action()). + if let Some(model) = env_model.as_mut() { + let resets_sigpipe = opts.default_signal.apply_all + || opts + .default_signal + .signals + .contains(&(libc::SIGPIPE as usize)); + if resets_sigpipe { + set_env_entry(model, OsStr::new("RUST_SIGPIPE"), OsStr::new("default")); + } + } } apply_change_directory(&opts)?; if opts.program.is_empty() { // no program provided, so just dump all env vars to stdout - print_all_env_vars(opts.line_ending)?; + if let Some(model) = &env_model { + print_env_model(model, opts.line_ending)?; + } else { + print_all_env_vars(opts.line_ending)?; + } } else { - return self.run_program(&opts, self.do_debug_printing); + return self.run_program(&opts, self.do_debug_printing, env_model.as_deref()); } Ok(()) @@ -841,6 +1085,7 @@ impl EnvAppData { &mut self, opts: &Options<'_>, do_debug_printing: bool, + env_model: Option<&[OsString]>, ) -> Result<(), Box> { let prog = Cow::from(opts.program[0]); @@ -903,6 +1148,40 @@ impl EnvAppData { argv.push(arg_cstring); } + if let Some(model) = env_model { + #[cfg(all(unix, not(target_os = "fuchsia")))] + { + // Replace the process environment with the model so exec + // gets it verbatim: order, duplicates and entries without + // a '=' byte are all preserved. PATH used for the exec + // search is then read from the model as well. The exec + // happens before the C strings holding the environment go + // out of scope. + let env_cstrings: Vec = model + .iter() + .map(|entry| CString::new(entry.as_bytes())) + .collect::>() + .map_err(|_| self.make_error_no_such_file_or_dir(&prog))?; + let mut env_ptrs: Vec<*mut libc::c_char> = + env_cstrings.iter().map(|c| c.as_ptr().cast_mut()).collect(); + env_ptrs.push(core::ptr::null_mut()); + unsafe { + environ = env_ptrs.as_mut_ptr(); + } + match execvp(&prog_cstring, &argv).unwrap_err() { + nix::errno::Errno::ENOENT => { + return Err(self.make_error_no_such_file_or_dir(&prog)); + } + e => { + uucore::show_error!("{}: {}", prog.quote(), strip_errno(&e.into())); + return Err(126.into()); + } + } + } + #[cfg(not(all(unix, not(target_os = "fuchsia"))))] + materialize_env_model(model); + } + // Execute the program using execvp. this replaces the current // process. The execvp function takes care of appending a NULL // argument to the argument list so that we don't have to. @@ -919,6 +1198,9 @@ impl EnvAppData { #[cfg(not(unix))] { // Fallback to Command::status for non-Unix systems + if let Some(model) = env_model { + materialize_env_model(model); + } let mut cmd = std::process::Command::new(&*prog); cmd.args(args); @@ -963,6 +1245,10 @@ fn make_options<'a>( let running_directory = matches .get_one::("chdir") .map(OsString::as_os_str); + let env0_from = match matches.get_many::(options::ENV0_FROM) { + Some(v) => v.map(OsString::as_os_str).collect(), + None => Vec::new(), + }; let unsets = match matches.get_many::("unset") { Some(v) => v.map(OsString::as_os_str).collect(), None => Vec::new(), @@ -984,6 +1270,7 @@ fn make_options<'a>( ignore_env, line_ending, running_directory, + env0_from, unsets, sets: vec![], program: vec![], @@ -1022,18 +1309,21 @@ fn make_options<'a>( Ok(opts) } +fn validate_unset_name(name: &OsStr) -> Result<(), Box> { + let native_name = NativeStr::new(name); + if name.is_empty() || native_name.contains('\0').unwrap() || native_name.contains('=').unwrap() + { + return Err(USimpleError::new( + 125, + translate!("env-error-cannot-unset-invalid", "name" => name.quote()), + )); + } + Ok(()) +} + fn apply_unset_env_vars(opts: &Options<'_>) -> Result<(), Box> { for name in &opts.unsets { - let native_name = NativeStr::new(name); - if name.is_empty() - || native_name.contains('\0').unwrap() - || native_name.contains('=').unwrap() - { - return Err(USimpleError::new( - 125, - translate!("env-error-cannot-unset-invalid", "name" => name.quote()), - )); - } + validate_unset_name(name)?; unsafe { env::remove_var(name); } 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/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/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..0727ee01302 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -512,7 +512,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 { 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/tail/src/chunks.rs b/src/uu/tail/src/chunks.rs index a14df677cf7..1446df03440 100644 --- a/src/uu/tail/src/chunks.rs +++ b/src/uu/tail/src/chunks.rs @@ -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/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/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/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_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_cp.rs b/tests/by-util/test_cp.rs index 54188b30dad..51426ddcbf2 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -5533,13 +5533,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 +5586,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 +5684,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 +5934,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 +6027,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() diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 048ed9c638e..74c22800d13 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.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) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho sighandler putenv - #![allow(clippy::missing_errors_doc)] #[cfg(unix)] @@ -395,6 +393,152 @@ fn test_fail_null_with_program() { .stderr_contains("cannot specify --null (-0) with command"); } +// spell-checker:ignore (words) noeq OLDY +#[test] +fn test_env0_from_roundtrip() { + let scene = TestScenario::new(util_name!()); + let contents = "A=1\0B=2\0A=3\0\0plain\0"; + scene.fixtures.write("env0", contents); + scene + .ucmd() + .args(&["-i", "--env0-from=env0", "--null"]) + .succeeds() + .no_stderr() + .stdout_is_bytes(contents.as_bytes()); +} + +#[test] +fn test_env0_from_empty_file() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("env0", ""); + scene + .ucmd() + .args(&["-i", "--env0-from", "env0", "--null"]) + .succeeds() + .no_stdout() + .no_stderr(); +} + +#[test] +fn test_env0_from_requires_nul_termination() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("env0", "A=1"); + scene + .ucmd() + .args(&["-i", "--env0-from", "env0"]) + .fails_with_code(1) + .no_stdout() + .stderr_is("env: 'env0': file must be empty or end with a NUL byte\n"); +} + +#[test] +fn test_env0_from_multiple_files_processed_in_order() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("first", "FOO=one\0BAR=1\0"); + scene.fixtures.write("second", "FOO=two\0"); + scene + .ucmd() + .args(&[ + "-i", + "--env0-from", + "first", + "--env0-from", + "second", + "--null", + ]) + .succeeds() + .no_stderr() + .stdout_is_bytes("FOO=two\0BAR=1\0".as_bytes()); +} + +#[test] +fn test_env0_from_missing_file() { + new_ucmd!() + .args(&["-i", "--env0-from", "no_such_env0"]) + .fails_with_code(1) + .no_stdout() + .stderr_contains("env: no_such_env0:"); +} + +#[test] +fn test_env0_from_stdin() { + new_ucmd!() + .args(&["-i", "--env0-from=-", "--null"]) + .pipe_in("X=1\0Y=2\0") + .succeeds() + .no_stderr() + .stdout_is_bytes("X=1\0Y=2\0".as_bytes()); +} + +#[test] +fn test_env0_from_merges_with_inherited() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("env0", "FOO=fromfile\0OLDY=1\0"); + let out = scene + .ucmd() + .env("FOO", "inherited") + .args(&["--env0-from", "env0"]) + .succeeds() + .stdout_move_str(); + assert!(out.lines().any(|l| l == "FOO=fromfile"), "got: {out}"); + assert!(!out.lines().any(|l| l == "FOO=inherited"), "got: {out}"); + assert!(out.lines().any(|l| l == "OLDY=1")); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_env0_from_exec_preserves_env() { + let scene = TestScenario::new(util_name!()); + let contents = "FOO=1\0FOO=2\0"; + scene.fixtures.write("env0", contents); + scene + .ucmd() + .args(&["-i", "--env0-from", "env0"]) + .arg(uutests::util::get_tests_binary()) + .args(&[util_name!(), "--null"]) + .succeeds() + .no_stderr() + .stdout_is_bytes(contents.as_bytes()); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_env0_from_passes_entries_without_equals_to_exec() { + let scene = TestScenario::new(util_name!()); + let contents = "PATH=/bin\0A=1\0\0noeq\0"; + scene.fixtures.write("env0", contents); + scene + .ucmd() + .args(&["-i", "--env0-from", "env0", "cat", "/proc/self/environ"]) + .succeeds() + .no_stderr() + .stdout_is_bytes(contents.as_bytes()); +} + +#[test] +fn test_env0_from_unset_applies_after() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("env0", "FOO=1\0FOO=2\0"); + scene + .ucmd() + .args(&["-i", "--env0-from", "env0", "-u", "FOO", "--null"]) + .succeeds() + .no_stdout() + .no_stderr(); +} + +#[test] +fn test_env0_from_set_applies_after() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("env0", "FOO=1\0FOO=2\0"); + scene + .ucmd() + .args(&["-i", "--env0-from", "env0", "--null", "FOO=3"]) + .succeeds() + .no_stderr() + .stdout_is_bytes("FOO=3\0FOO=2\0".as_bytes()); +} + #[cfg(not(windows))] #[test] fn test_change_directory() { diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index ecff20dd488..e0f47ae5cc4 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -506,32 +506,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_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_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_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(";;"); }