mirror of
https://github.com/uutils/coreutils.git
synced 2026-05-06 07:26:38 -04:00
Merge branch 'main' into core-freepfx
This commit is contained in:
+31
-23
@@ -4,7 +4,7 @@ name: CICD
|
||||
# spell-checker:ignore (env/flags) Awarnings Ccodegen Coverflow Cpanic Dwarnings RUSTDOCFLAGS RUSTFLAGS Zpanic CARGOFLAGS
|
||||
# spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata
|
||||
# spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd
|
||||
# spell-checker:ignore (shell/tools) binutils choco clippy dmake esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl mkdir nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs
|
||||
# spell-checker:ignore (shell/tools) binutils choco clippy dmake esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl mkdir nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs zstd
|
||||
# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed
|
||||
|
||||
env:
|
||||
@@ -297,7 +297,6 @@ jobs:
|
||||
mv -T target target.cache
|
||||
fi
|
||||
# Check that we don't cross-build uudoc
|
||||
# also do not try to generate manpages for part of hashsum
|
||||
env CARGO_BUILD_TARGET=aarch64-unknown-linux-gnu make install-manpages PREFIX=/tmp/usr UTILS=true
|
||||
# build (host)
|
||||
make build
|
||||
@@ -371,25 +370,19 @@ jobs:
|
||||
run: |
|
||||
set -x
|
||||
DESTDIR=/tmp/ make PROFILE=release MULTICALL=n install
|
||||
# Check that the utils are present
|
||||
test -f /tmp/usr/local/bin/hashsum
|
||||
# Check that hashsum symlinks are present
|
||||
test -h /tmp/usr/local/bin/b2sum
|
||||
test -h /tmp/usr/local/bin/md5sum
|
||||
test -h /tmp/usr/local/bin/sha1sum
|
||||
test -h /tmp/usr/local/bin/sha224sum
|
||||
test -h /tmp/usr/local/bin/sha256sum
|
||||
test -h /tmp/usr/local/bin/sha384sum
|
||||
test -h /tmp/usr/local/bin/sha512sum
|
||||
# Check that *sum are present
|
||||
for s in {md5,b2,sha1,sha224,sha256,sha384,sha512}sum
|
||||
do test -e /tmp/usr/local/bin/${s}
|
||||
done
|
||||
- name: "`make install MULTICALL=y LN=ln -svf`"
|
||||
shell: bash
|
||||
run: |
|
||||
set -x
|
||||
DESTDIR=/tmp/ make PROFILE=release MULTICALL=y LN="ln -svf" install
|
||||
# Check that relative symlinks of hashsum are present
|
||||
[ $(readlink /tmp/usr/local/bin/b2sum) = coreutils ]
|
||||
[ $(readlink /tmp/usr/local/bin/md5sum) = coreutils ]
|
||||
[ $(readlink /tmp/usr/local/bin/sha512sum) = coreutils ]
|
||||
# Check that symlinks of *sum are present
|
||||
for s in {md5,b2,sha1,sha224,sha256,sha384,sha512}sum
|
||||
do test $(readlink /tmp/usr/local/bin/${s}) = coreutils
|
||||
done
|
||||
- name: "`make UTILS=XXX`"
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -637,6 +630,8 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Avoid no space left on device
|
||||
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android &
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_MIN_SRV }}
|
||||
@@ -692,7 +687,7 @@ jobs:
|
||||
outputs TARGET_ARCH TARGET_OS
|
||||
# package name
|
||||
PKG_suffix=".tar.gz" ; case '${{ matrix.job.target }}' in *-pc-windows-*) PKG_suffix=".zip" ;; esac;
|
||||
PKG_BASENAME=${PROJECT_NAME}-${REF_TAG:-$REF_SHAS}-${{ matrix.job.target }}
|
||||
PKG_BASENAME=${PROJECT_NAME}-${{ matrix.job.target }}
|
||||
PKG_NAME=${PKG_BASENAME}${PKG_suffix}
|
||||
outputs PKG_suffix PKG_BASENAME PKG_NAME
|
||||
# deployable tag? (ie, leading "vM" or "M"; M == version number)
|
||||
@@ -861,8 +856,6 @@ jobs:
|
||||
if: matrix.job.skip-tests != true
|
||||
shell: bash
|
||||
run: |
|
||||
command -v sudo && sudo rm -rf /usr/local/lib/android /usr/share/dotnet # avoid no space left
|
||||
df -h ||:
|
||||
## Test individual utilities
|
||||
${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} test --target=${{ matrix.job.target }} \
|
||||
${{ matrix.job.cargo-options }} ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }}
|
||||
@@ -897,6 +890,20 @@ jobs:
|
||||
*) tar czf '${{ steps.vars.outputs.PKG_NAME }}' '${{ steps.vars.outputs.PKG_BASENAME }}'/* ;;
|
||||
esac
|
||||
popd >/dev/null
|
||||
- name: Package manpages and completions
|
||||
if: matrix.job.target == 'x86_64-unknown-linux-gnu' && matrix.job.features == 'feat_os_unix,uudoc'
|
||||
run: |
|
||||
mkdir -p share/{man/man1,bash-completion/completions,fish/vendor_completions.d,zsh/site-functions,elvish/lib}
|
||||
_uudoc=target/${{ matrix.job.target }}/release/uudoc
|
||||
for bin in $('target/${{ matrix.job.target }}/release/coreutils' --list) coreutils;do
|
||||
${_uudoc} manpage ${bin} > share/man/man1/${bin}.1
|
||||
${_uudoc} completion ${bin} bash > share/bash-completion/completions/${bin}.bash
|
||||
${_uudoc} completion ${bin} fish > share/fish/vendor_completions.d/${bin}.fish
|
||||
${_uudoc} completion ${bin} zsh > share/zsh/site-functions/_${bin}
|
||||
${_uudoc} completion ${bin} elvish > share/elvish/lib/${bin}.elv
|
||||
done
|
||||
rm share/zsh/site-functions/_[ # not supported
|
||||
tar --zstd -cf docs.tar.zst share
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: steps.vars.outputs.DEPLOY && matrix.job.skip-publish != true
|
||||
@@ -904,18 +911,19 @@ jobs:
|
||||
draft: true
|
||||
files: |
|
||||
${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }}
|
||||
docs.tar.zst
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Publish latest commit
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: steps.vars.outputs.DEPLOY && matrix.job.skip-publish != true
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.job.skip-publish != true
|
||||
with:
|
||||
tag_name: latest-commit
|
||||
force_update: true
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: |
|
||||
${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }}
|
||||
docs.tar.zst
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1264,13 +1272,13 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Avoid no space left on device
|
||||
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android &
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: build and test all features individually
|
||||
shell: bash
|
||||
run: |
|
||||
command -v sudo && sudo rm -rf /usr/local/lib/android /usr/share/dotnet # avoid no space left
|
||||
df -h ||:
|
||||
CARGO_FEATURES_OPTION='--features=${{ matrix.job.features }}' ;
|
||||
for f in $(util/show-utils.sh ${CARGO_FEATURES_OPTION})
|
||||
do
|
||||
|
||||
@@ -233,15 +233,6 @@ jobs:
|
||||
lima ls -laZ /etc/selinux
|
||||
lima sudo sestatus
|
||||
|
||||
# Ensure we're running in enforcing mode
|
||||
lima sudo setenforce 1
|
||||
lima getenforce
|
||||
|
||||
# Create test files with SELinux contexts for testing
|
||||
lima sudo mkdir -p /var/test_selinux
|
||||
lima sudo touch /var/test_selinux/test_file
|
||||
lima sudo chcon -t etc_t /var/test_selinux/test_file
|
||||
lima ls -Z /var/test_selinux/test_file # Verify context
|
||||
- name: Install dependencies in VM
|
||||
run: |
|
||||
lima sudo dnf -y update
|
||||
@@ -267,8 +258,16 @@ jobs:
|
||||
lima bash -c "cd ~/work/uutils/ && echo 'Found SELinux tests:'; wc -l selinux-tests.txt"
|
||||
- name: Run GNU SELinux tests
|
||||
run: |
|
||||
# Ensure we're running in enforcing mode
|
||||
lima sudo setenforce 1
|
||||
lima getenforce
|
||||
|
||||
# Create test files with SELinux contexts for testing
|
||||
lima sudo mkdir -p /var/test_selinux
|
||||
lima sudo touch /var/test_selinux/test_file
|
||||
lima sudo chcon -t etc_t /var/test_selinux/test_file
|
||||
lima ls -Z /var/test_selinux/test_file # Verify context
|
||||
|
||||
lima cat /proc/filesystems
|
||||
lima bash -c "cd ~/work/uutils/ && bash util/run-gnu-test.sh \$(cat selinux-tests.txt)"
|
||||
- name: Extract testing info from individual logs into JSON
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Avoid no space left on device (Ubuntu runner)
|
||||
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android
|
||||
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android &
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run sccache-cache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
@@ -8,3 +8,4 @@ tests/tty/tty-eof
|
||||
tests/misc/stdbuf
|
||||
tests/misc/usage_vs_getopt
|
||||
tests/misc/tee
|
||||
tests/tail/follow-name
|
||||
|
||||
@@ -184,6 +184,7 @@ inacc
|
||||
maint
|
||||
proc
|
||||
procs
|
||||
TOCTOU
|
||||
|
||||
# * constants
|
||||
xffff
|
||||
@@ -202,6 +203,7 @@ nofield
|
||||
# * clippy
|
||||
uninlined
|
||||
nonminimal
|
||||
rposition
|
||||
|
||||
# * CPU/hardware features
|
||||
ASIMD
|
||||
|
||||
+4
-4
@@ -78,11 +78,11 @@ issues and writing documentation are just as important as writing code.
|
||||
We can't fix bugs we don't know about, so good issues are super helpful! Here
|
||||
are some tips for writing good issues:
|
||||
|
||||
- If you find a bug, make sure it's still a problem on the `main` branch.
|
||||
- If you find a bug, make sure it's still a problem on the [`main` branch](https://github.com/uutils/coreutils/releases/tag/latest-commit).
|
||||
- Search through the existing issues to see whether it has already been
|
||||
reported.
|
||||
- Make sure to include all relevant information, such as:
|
||||
- Which version of uutils did you check?
|
||||
- Which version or commit hash of uutils did you check?
|
||||
- Which version of GNU coreutils are you comparing with?
|
||||
- What platform are you on?
|
||||
- Provide a way to reliably reproduce the issue.
|
||||
@@ -250,8 +250,8 @@ gitignore: add temporary files
|
||||
- It's up to you whether you want to use `git merge main` or
|
||||
`git rebase main`.
|
||||
- Feel free to ask for help with merge conflicts.
|
||||
- You do not need to ping maintainers to request a review, but it's fine to do
|
||||
so if you don't get a response within a few days.
|
||||
- You do not need to ping maintainers to request a review immediately after submission. If you do not get a response to your patch within a few days, it is fine to request a review.
|
||||
- If after a week your patch has still not been reviewed, we recommend that you ping the maintainers on our Discord channel in `#coreutils-chat`.
|
||||
|
||||
## Platforms
|
||||
|
||||
|
||||
@@ -549,6 +549,7 @@ uutests.workspace = true
|
||||
uucore = { workspace = true, features = [
|
||||
"mode",
|
||||
"entries",
|
||||
"pipes",
|
||||
"process",
|
||||
"signals",
|
||||
"utmpx",
|
||||
|
||||
@@ -29,8 +29,9 @@ options might be missing or different behavior might be experienced.
|
||||
|
||||
<div class="oranda-hide">
|
||||
|
||||
We provide prebuilt binaries at https://github.com/uutils/coreutils/releases/latest .
|
||||
It is recommended to install from main branch if you install from source.
|
||||
We provide prebuilt binaries, manpages, and shell completions from main branch at https://github.com/uutils/coreutils/releases/tag/latest-commit .
|
||||
The latest stable tag https://github.com/uutils/coreutils/releases/latest exists only for reproducible products and packagers.
|
||||
You should use binary from latest commit generally.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -136,6 +136,11 @@ pub fn are_files_identical(path1: &Path, path2: &Path) -> io::Result<bool> {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// only proceed if both are regular files
|
||||
if !metadata1.is_file() || !metadata2.is_file() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let file1 = File::open(path1)?;
|
||||
let file2 = File::open(path2)?;
|
||||
|
||||
|
||||
@@ -82,20 +82,25 @@ fn cp_preserve_metadata(
|
||||
|
||||
#[divan::bench(args = [16])]
|
||||
fn cp_large_file(bencher: Bencher, size_mb: usize) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let source = temp_dir.path().join("source.bin");
|
||||
let dest = temp_dir.path().join("dest.bin");
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let source = temp_dir.path().join("source.bin");
|
||||
binary_data::create_file(&source, size_mb, b'x');
|
||||
(temp_dir, source)
|
||||
})
|
||||
.counter(divan::counter::BytesCount::new(size_mb * 1024 * 1024))
|
||||
.bench_values(|(temp_dir, source)| {
|
||||
// Use unique destination name to avoid filesystem allocation variance
|
||||
let dest = temp_dir.path().join(format!(
|
||||
"dest_{}.bin",
|
||||
std::ptr::addr_of!(temp_dir) as usize
|
||||
));
|
||||
let source_str = source.to_str().unwrap();
|
||||
let dest_str = dest.to_str().unwrap();
|
||||
|
||||
binary_data::create_file(&source, size_mb, b'x');
|
||||
|
||||
let source_str = source.to_str().unwrap();
|
||||
let dest_str = dest.to_str().unwrap();
|
||||
|
||||
bencher.bench(|| {
|
||||
fs_utils::remove_path(&dest);
|
||||
|
||||
black_box(run_util_function(uumain, &[source_str, dest_str]));
|
||||
});
|
||||
black_box(run_util_function(uumain, &[source_str, dest_str]));
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
+4
-3
@@ -30,7 +30,7 @@ use std::cmp;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Stdout, Write};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::fd::AsFd;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
@@ -601,7 +601,7 @@ enum Density {
|
||||
/// Data destinations.
|
||||
enum Dest {
|
||||
/// Output to stdout.
|
||||
Stdout(Stdout),
|
||||
Stdout(File),
|
||||
|
||||
/// Output to a file.
|
||||
///
|
||||
@@ -829,7 +829,8 @@ struct Output<'a> {
|
||||
impl<'a> Output<'a> {
|
||||
/// Instantiate this struct with stdout as a destination.
|
||||
fn new_stdout(settings: &'a Settings) -> UResult<Self> {
|
||||
let mut dst = Dest::Stdout(io::stdout());
|
||||
let fx = OwnedFileDescriptorOrHandle::from(io::stdout())?;
|
||||
let mut dst = Dest::Stdout(fx.into_file());
|
||||
dst.seek(settings.seek, settings.obs)
|
||||
.map_err_context(|| translate!("dd-error-write-error"))?;
|
||||
Ok(Self { dst, settings })
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
#[cfg(unix)]
|
||||
use uucore::display::print_verbatim;
|
||||
use uucore::error::{UResult, UUsageError};
|
||||
use uucore::format_usage;
|
||||
@@ -18,51 +19,84 @@ mod options {
|
||||
pub const DIR: &str = "dir";
|
||||
}
|
||||
|
||||
/// Handle the special case where a path ends with "/."
|
||||
/// Perform dirname as pure string manipulation per POSIX/GNU behavior.
|
||||
///
|
||||
/// dirname should NOT normalize paths. It does simple string manipulation:
|
||||
/// 1. Strip trailing slashes (unless path is all slashes)
|
||||
/// 2. If ends with `/.` (possibly `//.` or `///.`), strip the `/+.` pattern
|
||||
/// 3. Otherwise, remove everything after the last `/`
|
||||
/// 4. If no `/` found, return `.`
|
||||
/// 5. Strip trailing slashes from result (unless result would be empty)
|
||||
///
|
||||
/// Examples:
|
||||
/// - `foo/.` → `foo`
|
||||
/// - `foo/./bar` → `foo/.`
|
||||
/// - `foo/bar` → `foo`
|
||||
/// - `a/b/c` → `a/b`
|
||||
///
|
||||
/// This matches GNU/POSIX behavior where `dirname("/home/dos/.")` returns "/home/dos"
|
||||
/// rather than "/home" (which would be the result of `Path::parent()` due to normalization).
|
||||
/// Per POSIX.1-2017 dirname specification and GNU coreutils manual:
|
||||
/// - POSIX: <https://pubs.opengroup.org/onlinepubs/9699919799/utilities/dirname.html>
|
||||
/// - GNU: <https://www.gnu.org/software/coreutils/manual/html_node/dirname-invocation.html>
|
||||
///
|
||||
/// dirname should do simple string manipulation without path normalization.
|
||||
/// See issue #8910 and similar fix in basename (#8373, commit c5268a897).
|
||||
///
|
||||
/// Returns `Some(())` if the special case was handled (output already printed),
|
||||
/// or `None` if normal `Path::parent()` logic should be used.
|
||||
fn handle_trailing_dot(path_bytes: &[u8]) -> Option<()> {
|
||||
if !path_bytes.ends_with(b"/.") {
|
||||
return None;
|
||||
fn dirname_string_manipulation(path_bytes: &[u8]) -> Cow<'_, [u8]> {
|
||||
if path_bytes.is_empty() {
|
||||
return Cow::Borrowed(b".");
|
||||
}
|
||||
|
||||
// Strip the "/." suffix and print the result
|
||||
if path_bytes.len() == 2 {
|
||||
// Special case: "/." -> "/"
|
||||
print!("/");
|
||||
Some(())
|
||||
} else {
|
||||
// General case: "/home/dos/." -> "/home/dos"
|
||||
let stripped = &path_bytes[..path_bytes.len() - 2];
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let result = std::ffi::OsStr::from_bytes(stripped);
|
||||
print_verbatim(result).unwrap();
|
||||
Some(())
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix, fall back to lossy conversion
|
||||
if let Ok(s) = std::str::from_utf8(stripped) {
|
||||
print!("{s}");
|
||||
Some(())
|
||||
} else {
|
||||
// Can't handle non-UTF-8 on non-Unix, fall through to normal logic
|
||||
None
|
||||
let mut bytes = path_bytes;
|
||||
|
||||
// Step 1: Strip trailing slashes (but not if the entire path is slashes)
|
||||
let all_slashes = bytes.iter().all(|&b| b == b'/');
|
||||
if all_slashes {
|
||||
return Cow::Borrowed(b"/");
|
||||
}
|
||||
|
||||
while bytes.len() > 1 && bytes.ends_with(b"/") {
|
||||
bytes = &bytes[..bytes.len() - 1];
|
||||
}
|
||||
|
||||
// Step 2: Check if it ends with `/.` and strip the `/+.` pattern
|
||||
if bytes.ends_with(b".") && bytes.len() >= 2 {
|
||||
let dot_pos = bytes.len() - 1;
|
||||
if bytes[dot_pos - 1] == b'/' {
|
||||
// Find where the slashes before the dot start
|
||||
let mut slash_start = dot_pos - 1;
|
||||
while slash_start > 0 && bytes[slash_start - 1] == b'/' {
|
||||
slash_start -= 1;
|
||||
}
|
||||
// Return the stripped result
|
||||
if slash_start == 0 {
|
||||
// Result would be empty
|
||||
return if path_bytes.starts_with(b"/") {
|
||||
Cow::Borrowed(b"/")
|
||||
} else {
|
||||
Cow::Borrowed(b".")
|
||||
};
|
||||
}
|
||||
return Cow::Borrowed(&bytes[..slash_start]);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Normal dirname - find last / and remove everything after it
|
||||
if let Some(last_slash_pos) = bytes.iter().rposition(|&b| b == b'/') {
|
||||
// Found a slash, remove everything after it
|
||||
let mut result = &bytes[..last_slash_pos];
|
||||
|
||||
// Strip trailing slashes from result (but keep at least one if at the start)
|
||||
while result.len() > 1 && result.ends_with(b"/") {
|
||||
result = &result[..result.len() - 1];
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
return Cow::Borrowed(b"/");
|
||||
}
|
||||
|
||||
return Cow::Borrowed(result);
|
||||
}
|
||||
|
||||
// No slash found, return "."
|
||||
Cow::Borrowed(b".")
|
||||
}
|
||||
|
||||
#[uucore::main]
|
||||
@@ -83,27 +117,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
||||
for path in &dirnames {
|
||||
let path_bytes = uucore::os_str_as_bytes(path.as_os_str()).unwrap_or(&[]);
|
||||
let result = dirname_string_manipulation(path_bytes);
|
||||
|
||||
if handle_trailing_dot(path_bytes).is_none() {
|
||||
// Normal path handling using Path::parent()
|
||||
let p = Path::new(path);
|
||||
match p.parent() {
|
||||
Some(d) => {
|
||||
if d.components().next().is_none() {
|
||||
print!(".");
|
||||
} else {
|
||||
print_verbatim(d).unwrap();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if p.is_absolute() || path.as_os_str() == "/" {
|
||||
print!("/");
|
||||
} else {
|
||||
print!(".");
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let result_os = std::ffi::OsStr::from_bytes(&result);
|
||||
print_verbatim(result_os).unwrap();
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix, fall back to lossy conversion
|
||||
if let Ok(s) = std::str::from_utf8(&result) {
|
||||
print!("{s}");
|
||||
} else {
|
||||
// Fallback for non-UTF-8 paths on non-Unix systems
|
||||
print!(".");
|
||||
}
|
||||
}
|
||||
|
||||
print!("{line_ending}");
|
||||
}
|
||||
|
||||
|
||||
@@ -61,25 +61,49 @@ fn du_human_balanced_tree(
|
||||
/// Benchmark du on wide directory structures (many files/dirs, shallow)
|
||||
#[divan::bench(args = [(5000, 500)])]
|
||||
fn du_wide_tree(bencher: Bencher, (total_files, total_dirs): (usize, usize)) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
fs_tree::create_wide_tree(temp_dir.path(), total_files, total_dirs);
|
||||
bench_du_with_args(bencher, &temp_dir, &[]);
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
fs_tree::create_wide_tree(temp_dir.path(), total_files, total_dirs);
|
||||
temp_dir
|
||||
})
|
||||
.bench_values(|temp_dir| {
|
||||
let temp_path_str = temp_dir.path().to_str().unwrap();
|
||||
let args = vec![temp_path_str];
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark du -a on wide directory structures
|
||||
#[divan::bench(args = [(5000, 500)])]
|
||||
fn du_all_wide_tree(bencher: Bencher, (total_files, total_dirs): (usize, usize)) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
fs_tree::create_wide_tree(temp_dir.path(), total_files, total_dirs);
|
||||
bench_du_with_args(bencher, &temp_dir, &["-a"]);
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
fs_tree::create_wide_tree(temp_dir.path(), total_files, total_dirs);
|
||||
temp_dir
|
||||
})
|
||||
.bench_values(|temp_dir| {
|
||||
let temp_path_str = temp_dir.path().to_str().unwrap();
|
||||
let args = vec![temp_path_str, "-a"];
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark du on deep directory structures
|
||||
#[divan::bench(args = [(100, 3)])]
|
||||
fn du_deep_tree(bencher: Bencher, (depth, files_per_level): (usize, usize)) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
fs_tree::create_deep_tree(temp_dir.path(), depth, files_per_level);
|
||||
bench_du_with_args(bencher, &temp_dir, &[]);
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
fs_tree::create_deep_tree(temp_dir.path(), depth, files_per_level);
|
||||
temp_dir
|
||||
})
|
||||
.bench_values(|temp_dir| {
|
||||
let temp_path_str = temp_dir.path().to_str().unwrap();
|
||||
let args = vec![temp_path_str];
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark du -s (summarize) on balanced tree
|
||||
|
||||
@@ -30,7 +30,7 @@ install-error-chown-failed = failed to chown { $path }: { $error }
|
||||
install-error-invalid-target = invalid target { $path }: No such file or directory
|
||||
install-error-target-not-dir = target { $path } is not a directory
|
||||
install-error-backup-failed = cannot backup { $from } to { $to }
|
||||
install-error-install-failed = cannot install { $from } to { $to }
|
||||
install-error-install-failed = cannot install { $from } to { $to }: { $error }
|
||||
install-error-strip-failed = strip program failed: { $error }
|
||||
install-error-strip-abnormal = strip process terminated abnormally - exit code: { $code }
|
||||
install-error-metadata-failed = metadata error
|
||||
|
||||
@@ -30,7 +30,7 @@ install-error-chown-failed = échec du chown { $path } : { $error }
|
||||
install-error-invalid-target = cible invalide { $path } : Aucun fichier ou répertoire de ce type
|
||||
install-error-target-not-dir = la cible { $path } n'est pas un répertoire
|
||||
install-error-backup-failed = impossible de sauvegarder { $from } vers { $to }
|
||||
install-error-install-failed = impossible d'installer { $from } vers { $to }
|
||||
install-error-install-failed = impossible d'installer { $from } vers { $to }: { $error }
|
||||
install-error-strip-failed = échec du programme strip : { $error }
|
||||
install-error-strip-abnormal = le processus strip s'est terminé anormalement - code de sortie : { $code }
|
||||
install-error-metadata-failed = erreur de métadonnées
|
||||
|
||||
@@ -14,8 +14,8 @@ use filetime::{FileTime, set_file_times};
|
||||
use selinux::SecurityContext;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Debug;
|
||||
use std::fs::File;
|
||||
use std::fs::{self, metadata};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::path::{MAIN_SEPARATOR, Path, PathBuf};
|
||||
use std::process;
|
||||
use thiserror::Error;
|
||||
@@ -36,7 +36,7 @@ use uucore::translate;
|
||||
use uucore::{format_usage, show, show_error, show_if_err};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
|
||||
@@ -88,8 +88,8 @@ enum InstallError {
|
||||
#[error("{}", translate!("install-error-backup-failed", "from" => .0.quote(), "to" => .1.quote()))]
|
||||
BackupFailed(PathBuf, PathBuf, #[source] std::io::Error),
|
||||
|
||||
#[error("{}", translate!("install-error-install-failed", "from" => .0.quote(), "to" => .1.quote()))]
|
||||
InstallFailed(PathBuf, PathBuf, #[source] std::io::Error),
|
||||
#[error("{}", translate!("install-error-install-failed", "from" => .0.quote(), "to" => .1.quote(), "error" => .2.clone()))]
|
||||
InstallFailed(PathBuf, PathBuf, String),
|
||||
|
||||
#[error("{}", translate!("install-error-strip-failed", "error" => .0.clone()))]
|
||||
StripProgramFailed(String),
|
||||
@@ -796,22 +796,6 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult<Option<PathBuf>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a non-special file using [`fs::copy`].
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `from` - The source file path.
|
||||
/// * `to` - The destination file path.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns an empty Result or an error in case of failure.
|
||||
fn copy_normal_file(from: &Path, to: &Path) -> UResult<()> {
|
||||
if let Err(err) = fs::copy(from, to) {
|
||||
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy a file from one path to another. Handles the certain cases of special
|
||||
/// files (e.g character specials).
|
||||
///
|
||||
@@ -838,8 +822,10 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> {
|
||||
)
|
||||
.into());
|
||||
}
|
||||
// fs::copy fails if destination is a invalid symlink.
|
||||
// so lets just remove all existing files at destination before copy.
|
||||
|
||||
// Remove existing file at destination to allow overwriting
|
||||
// Note: create_new() below provides TOCTOU protection; if something
|
||||
// appears at this path between the remove and create, it will fail safely
|
||||
if let Err(e) = fs::remove_file(to) {
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
show_error!(
|
||||
@@ -849,25 +835,13 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let ft = match metadata(from) {
|
||||
Ok(ft) => ft.file_type(),
|
||||
Err(err) => {
|
||||
return Err(
|
||||
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let mut handle = File::open(from)?;
|
||||
// create_new provides TOCTOU protection
|
||||
let mut dest = OpenOptions::new().write(true).create_new(true).open(to)?;
|
||||
|
||||
// Stream-based copying to get around the limitations of std::fs::copy
|
||||
#[cfg(unix)]
|
||||
if ft.is_char_device() || ft.is_block_device() || ft.is_fifo() {
|
||||
let mut handle = File::open(from)?;
|
||||
let mut dest = File::create(to)?;
|
||||
copy_stream(&mut handle, &mut dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
copy_normal_file(from, to)?;
|
||||
copy_stream(&mut handle, &mut dest).map_err(|err| {
|
||||
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err.to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+9
-4
@@ -3398,10 +3398,15 @@ fn display_item_name(
|
||||
}
|
||||
}
|
||||
|
||||
match fs::metadata(&absolute_target) {
|
||||
Ok(_) => {
|
||||
let target_data =
|
||||
PathData::new(absolute_target, None, None, config, false);
|
||||
match fs::canonicalize(&absolute_target) {
|
||||
Ok(resolved_target) => {
|
||||
let target_data = PathData::new(
|
||||
resolved_target,
|
||||
None,
|
||||
target_path.file_name().map(|s| s.to_os_string()),
|
||||
config,
|
||||
false,
|
||||
);
|
||||
name.push(color_name(
|
||||
escaped_target,
|
||||
&target_data,
|
||||
|
||||
@@ -10,96 +10,120 @@ use uucore::benchmark::run_util_function;
|
||||
/// Benchmark SI formatting by passing numbers as command-line arguments
|
||||
#[divan::bench(args = [10_000])]
|
||||
fn numfmt_to_si(bencher: Bencher, count: usize) {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args = vec!["--to=si"];
|
||||
let number_refs: Vec<&str> = numbers.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(number_refs);
|
||||
|
||||
bencher.bench(|| {
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args: Vec<String> = vec!["--to=si".to_string()];
|
||||
args.extend(numbers);
|
||||
args
|
||||
})
|
||||
.bench_values(|args| {
|
||||
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
black_box(run_util_function(uumain, &arg_refs));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark SI formatting with precision format
|
||||
#[divan::bench(args = [10_000])]
|
||||
fn numfmt_to_si_precision(bencher: Bencher, count: usize) {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args = vec!["--to=si", "--format=%.6f"];
|
||||
let number_refs: Vec<&str> = numbers.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(number_refs);
|
||||
|
||||
bencher.bench(|| {
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args: Vec<String> = vec!["--to=si".to_string(), "--format=%.6f".to_string()];
|
||||
args.extend(numbers);
|
||||
args
|
||||
})
|
||||
.bench_values(|args| {
|
||||
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
black_box(run_util_function(uumain, &arg_refs));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark IEC (binary) formatting
|
||||
#[divan::bench(args = [10_000])]
|
||||
fn numfmt_to_iec(bencher: Bencher, count: usize) {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args = vec!["--to=iec"];
|
||||
let number_refs: Vec<&str> = numbers.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(number_refs);
|
||||
|
||||
bencher.bench(|| {
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args: Vec<String> = vec!["--to=iec".to_string()];
|
||||
args.extend(numbers);
|
||||
args
|
||||
})
|
||||
.bench_values(|args| {
|
||||
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
black_box(run_util_function(uumain, &arg_refs));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark parsing from SI format back to raw numbers
|
||||
#[divan::bench(args = [10_000])]
|
||||
fn numfmt_from_si(bencher: Bencher, count: usize) {
|
||||
// Generate SI formatted data (e.g., "1K", "2K", etc.)
|
||||
let numbers: Vec<String> = (1..=count).map(|n| format!("{n}K")).collect();
|
||||
let mut args = vec!["--from=si"];
|
||||
let number_refs: Vec<&str> = numbers.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(number_refs);
|
||||
|
||||
bencher.bench(|| {
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
// Generate SI formatted data (e.g., "1K", "2K", etc.)
|
||||
let numbers: Vec<String> = (1..=count).map(|n| format!("{n}K")).collect();
|
||||
let mut args: Vec<String> = vec!["--from=si".to_string()];
|
||||
args.extend(numbers);
|
||||
args
|
||||
})
|
||||
.bench_values(|args| {
|
||||
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
black_box(run_util_function(uumain, &arg_refs));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark large numbers with SI formatting
|
||||
#[divan::bench(args = [10_000])]
|
||||
fn numfmt_large_numbers_si(bencher: Bencher, count: usize) {
|
||||
// Generate larger numbers (millions to billions range)
|
||||
let numbers: Vec<String> = (1..=count).map(|n| (n * 1_000_000).to_string()).collect();
|
||||
let mut args = vec!["--to=si"];
|
||||
let number_refs: Vec<&str> = numbers.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(number_refs);
|
||||
|
||||
bencher.bench(|| {
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
// Generate numbers that all produce uniform SI output lengths (all in 1-9M range)
|
||||
// This avoids variance from variable output string lengths
|
||||
let numbers: Vec<String> = (1..=count)
|
||||
.map(|n| ((n % 9) + 1) * 1_000_000)
|
||||
.map(|n| n.to_string())
|
||||
.collect();
|
||||
let mut args: Vec<String> = vec!["--to=si".to_string()];
|
||||
args.extend(numbers);
|
||||
args
|
||||
})
|
||||
.bench_values(|args| {
|
||||
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
black_box(run_util_function(uumain, &arg_refs));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark different padding widths
|
||||
#[divan::bench(args = [(10_000, 50)])]
|
||||
fn numfmt_padding(bencher: Bencher, (count, padding): (usize, usize)) {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let padding_arg = format!("--padding={padding}");
|
||||
let mut args = vec!["--to=si", &padding_arg];
|
||||
let number_refs: Vec<&str> = numbers.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(number_refs);
|
||||
|
||||
bencher.bench(|| {
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args: Vec<String> = vec!["--to=si".to_string(), format!("--padding={padding}")];
|
||||
args.extend(numbers);
|
||||
args
|
||||
})
|
||||
.bench_values(|args| {
|
||||
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
black_box(run_util_function(uumain, &arg_refs));
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark round modes with SI formatting
|
||||
#[divan::bench(args = [("up", 10_000), ("down", 10_000), ("towards-zero", 10_000)])]
|
||||
fn numfmt_round_modes(bencher: Bencher, (round_mode, count): (&str, usize)) {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let round_arg = format!("--round={round_mode}");
|
||||
let mut args = vec!["--to=si", &round_arg];
|
||||
let number_refs: Vec<&str> = numbers.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(number_refs);
|
||||
|
||||
bencher.bench(|| {
|
||||
black_box(run_util_function(uumain, &args));
|
||||
});
|
||||
bencher
|
||||
.with_inputs(|| {
|
||||
let numbers: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
|
||||
let mut args: Vec<String> =
|
||||
vec!["--to=si".to_string(), format!("--round={round_mode}")];
|
||||
args.extend(numbers);
|
||||
args
|
||||
})
|
||||
.bench_values(|args| {
|
||||
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
black_box(run_util_function(uumain, &arg_refs));
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
+15
-28
@@ -549,19 +549,8 @@ fn is_writable_metadata(metadata: &Metadata) -> bool {
|
||||
(mode & 0o200) > 0
|
||||
}
|
||||
|
||||
/// Whether the given file or directory is writable.
|
||||
#[cfg(unix)]
|
||||
fn is_writable(path: &Path) -> bool {
|
||||
match fs::metadata(path) {
|
||||
Err(_) => false,
|
||||
Ok(metadata) => is_writable_metadata(&metadata),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the given file or directory is writable.
|
||||
#[cfg(not(unix))]
|
||||
fn is_writable(_path: &Path) -> bool {
|
||||
// TODO Not yet implemented.
|
||||
fn is_writable_metadata(_metadata: &Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -799,35 +788,33 @@ fn prompt_file(path: &Path, options: &Options) -> bool {
|
||||
if options.interactive == InteractiveMode::Never {
|
||||
return true;
|
||||
}
|
||||
// If interactive is Always we want to check if the file is symlink to prompt the right message
|
||||
if options.interactive == InteractiveMode::Always {
|
||||
if let Ok(metadata) = fs::symlink_metadata(path) {
|
||||
if metadata.is_symlink() {
|
||||
return prompt_yes!("remove symbolic link {}?", path.quote());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Ok(metadata) = fs::metadata(path) else {
|
||||
let Ok(metadata) = fs::symlink_metadata(path) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
if options.interactive == InteractiveMode::Always && is_writable(path) {
|
||||
if metadata.is_symlink() {
|
||||
return options.interactive != InteractiveMode::Always
|
||||
|| prompt_yes!("remove symbolic link {}?", path.quote());
|
||||
}
|
||||
|
||||
if options.interactive == InteractiveMode::Always && is_writable_metadata(&metadata) {
|
||||
return if metadata.len() == 0 {
|
||||
prompt_yes!("remove regular empty file {}?", path.quote())
|
||||
} else {
|
||||
prompt_yes!("remove file {}?", path.quote())
|
||||
};
|
||||
}
|
||||
prompt_file_permission_readonly(path, options)
|
||||
|
||||
prompt_file_permission_readonly(path, options, &metadata)
|
||||
}
|
||||
|
||||
fn prompt_file_permission_readonly(path: &Path, options: &Options) -> bool {
|
||||
fn prompt_file_permission_readonly(path: &Path, options: &Options, metadata: &Metadata) -> bool {
|
||||
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
|
||||
match (stdin_ok, fs::metadata(path), options.interactive) {
|
||||
(false, _, InteractiveMode::PromptProtected) => true,
|
||||
(_, Ok(_), _) if is_writable(path) => true,
|
||||
(_, Ok(metadata), _) if metadata.len() == 0 => prompt_yes!(
|
||||
match (stdin_ok, options.interactive) {
|
||||
(false, InteractiveMode::PromptProtected) => true,
|
||||
_ if is_writable_metadata(metadata) => true,
|
||||
_ if metadata.len() == 0 => prompt_yes!(
|
||||
"remove write-protected regular empty file {}?",
|
||||
path.quote()
|
||||
),
|
||||
|
||||
@@ -19,6 +19,9 @@ workspace = true
|
||||
[lib]
|
||||
path = "src/sort.rs"
|
||||
|
||||
[features]
|
||||
i18n-collator = ["uucore/i18n-collator"]
|
||||
|
||||
[dependencies]
|
||||
bigdecimal = { workspace = true }
|
||||
binary-heap-plus = { workspace = true }
|
||||
@@ -39,6 +42,7 @@ uucore = { workspace = true, features = [
|
||||
"parser-size",
|
||||
"version-cmp",
|
||||
"i18n-decimal",
|
||||
"i18n-collator",
|
||||
] }
|
||||
fluent = { workspace = true }
|
||||
|
||||
|
||||
+51
-10
@@ -23,6 +23,7 @@ use chunks::LineData;
|
||||
use clap::builder::ValueParser;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use custom_str_cmp::custom_str_cmp;
|
||||
|
||||
use ext_sort::ext_sort;
|
||||
use fnv::FnvHasher;
|
||||
use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, numeric_str_cmp};
|
||||
@@ -47,6 +48,8 @@ use uucore::error::{FromIo, strip_errno};
|
||||
use uucore::error::{UError, UResult, USimpleError, UUsageError};
|
||||
use uucore::extendedbigdecimal::ExtendedBigDecimal;
|
||||
use uucore::format_usage;
|
||||
#[cfg(feature = "i18n-collator")]
|
||||
use uucore::i18n::collator::locale_cmp;
|
||||
use uucore::i18n::decimal::locale_decimal_separator;
|
||||
use uucore::line_ending::LineEnding;
|
||||
use uucore::parser::num_parser::{ExtendedParser, ExtendedParserError};
|
||||
@@ -318,7 +321,10 @@ impl GlobalSettings {
|
||||
/// Precompute some data needed for sorting.
|
||||
/// This function **must** be called before starting to sort, and `GlobalSettings` may not be altered
|
||||
/// afterwards.
|
||||
fn init_precomputed(&mut self) {
|
||||
///
|
||||
/// When i18n-collator is enabled, `disable_fast_lexicographic` should be set to true if we're
|
||||
/// in a UTF-8 locale (to force locale-aware collation instead of byte comparison).
|
||||
fn init_precomputed(&mut self, disable_fast_lexicographic: bool) {
|
||||
self.precomputed.needs_tokens = self.selectors.iter().any(|s| s.needs_tokens);
|
||||
self.precomputed.selections_per_line =
|
||||
self.selectors.iter().filter(|s| s.needs_selection).count();
|
||||
@@ -333,11 +339,15 @@ impl GlobalSettings {
|
||||
.filter(|s| matches!(s.settings.mode, SortMode::GeneralNumeric))
|
||||
.count();
|
||||
|
||||
self.precomputed.fast_lexicographic = self.can_use_fast_lexicographic();
|
||||
self.precomputed.fast_lexicographic =
|
||||
!disable_fast_lexicographic && self.can_use_fast_lexicographic();
|
||||
self.precomputed.fast_ascii_insensitive = self.can_use_fast_ascii_insensitive();
|
||||
}
|
||||
|
||||
/// Returns true when the fast lexicographic path can be used safely.
|
||||
/// Note: When i18n-collator is enabled, the caller must have already determined
|
||||
/// whether locale-aware collation is needed (via checking if we're in a UTF-8 locale).
|
||||
/// This check is performed in uumain() before init_precomputed() is called.
|
||||
fn can_use_fast_lexicographic(&self) -> bool {
|
||||
self.mode == SortMode::Default
|
||||
&& !self.ignore_case
|
||||
@@ -2065,7 +2075,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
emit_debug_warnings(&settings, &global_flags, &legacy_warnings);
|
||||
}
|
||||
|
||||
settings.init_precomputed();
|
||||
// Initialize locale collation if needed (UTF-8 locales)
|
||||
// This MUST happen before init_precomputed() to avoid the performance regression
|
||||
#[cfg(feature = "i18n-collator")]
|
||||
let needs_locale_collation = uucore::i18n::collator::init_locale_collation();
|
||||
|
||||
#[cfg(not(feature = "i18n-collator"))]
|
||||
let needs_locale_collation = false;
|
||||
|
||||
settings.init_precomputed(needs_locale_collation);
|
||||
|
||||
let result = exec(&mut files, &settings, output, &mut tmp_dir);
|
||||
// Wait here if `SIGINT` was received,
|
||||
@@ -2446,13 +2464,36 @@ fn compare_by<'a>(
|
||||
}
|
||||
SortMode::Month => month_compare(a_str, b_str),
|
||||
SortMode::Version => version_cmp(a_str, b_str),
|
||||
SortMode::Default => custom_str_cmp(
|
||||
a_str,
|
||||
b_str,
|
||||
settings.ignore_non_printing,
|
||||
settings.dictionary_order,
|
||||
settings.ignore_case,
|
||||
),
|
||||
SortMode::Default => {
|
||||
// Use locale-aware comparison if feature is enabled and no custom flags are set
|
||||
#[cfg(feature = "i18n-collator")]
|
||||
{
|
||||
if settings.ignore_case
|
||||
|| settings.dictionary_order
|
||||
|| settings.ignore_non_printing
|
||||
{
|
||||
custom_str_cmp(
|
||||
a_str,
|
||||
b_str,
|
||||
settings.ignore_non_printing,
|
||||
settings.dictionary_order,
|
||||
settings.ignore_case,
|
||||
)
|
||||
} else {
|
||||
locale_cmp(a_str, b_str)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "i18n-collator"))]
|
||||
{
|
||||
custom_str_cmp(
|
||||
a_str,
|
||||
b_str,
|
||||
settings.ignore_non_printing,
|
||||
settings.dictionary_order,
|
||||
settings.ignore_case,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
if cmp != Ordering::Equal {
|
||||
return if settings.reverse { cmp.reverse() } else { cmp };
|
||||
|
||||
+19
-50
@@ -7,12 +7,14 @@
|
||||
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use std::ffi::OsString;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use tempfile::TempDir;
|
||||
use tempfile::tempdir;
|
||||
use thiserror::Error;
|
||||
use uucore::error::{FromIo, UResult, USimpleError, UUsageError};
|
||||
use uucore::error::{UResult, USimpleError, UUsageError};
|
||||
use uucore::format_usage;
|
||||
use uucore::parser::parse_size::parse_size_u64;
|
||||
use uucore::translate;
|
||||
@@ -208,55 +210,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
set_command_env(&mut command, "_STDBUF_E", &options.stderr);
|
||||
command.args(command_params);
|
||||
|
||||
let mut process = match command.spawn() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return match e.kind() {
|
||||
std::io::ErrorKind::PermissionDenied => Err(USimpleError::new(
|
||||
126,
|
||||
translate!("stdbuf-error-permission-denied"),
|
||||
)),
|
||||
std::io::ErrorKind::NotFound => Err(USimpleError::new(
|
||||
127,
|
||||
translate!("stdbuf-error-no-such-file"),
|
||||
)),
|
||||
_ => Err(USimpleError::new(
|
||||
1,
|
||||
translate!("stdbuf-error-failed-to-execute", "error" => e),
|
||||
)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let status = process.wait().map_err_context(String::new)?;
|
||||
match status.code() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(i.into())
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
let signal_msg = status
|
||||
.signal()
|
||||
.map_or_else(|| "unknown".to_string(), |s| s.to_string());
|
||||
Err(USimpleError::new(
|
||||
1,
|
||||
translate!("stdbuf-error-killed-by-signal", "signal" => signal_msg),
|
||||
))
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Err(USimpleError::new(
|
||||
1,
|
||||
"process terminated abnormally".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
// Replace the current process with the target program (no fork) using exec.
|
||||
let e = command.exec();
|
||||
// exec() only returns if there was an error
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::PermissionDenied => Err(USimpleError::new(
|
||||
126,
|
||||
translate!("stdbuf-error-permission-denied"),
|
||||
)),
|
||||
std::io::ErrorKind::NotFound => Err(USimpleError::new(
|
||||
127,
|
||||
translate!("stdbuf-error-no-such-file"),
|
||||
)),
|
||||
_ => Err(USimpleError::new(
|
||||
1,
|
||||
translate!("stdbuf-error-failed-to-execute", "error" => e),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -473,7 +473,9 @@ fn bounded_tail(file: &mut File, settings: &Settings) {
|
||||
return;
|
||||
}
|
||||
FilterMode::Bytes(Signum::Negative(count)) => {
|
||||
file.seek(SeekFrom::End(-(*count as i64))).unwrap();
|
||||
if file.seek(SeekFrom::End(-(*count as i64))).is_err() {
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
}
|
||||
limit = Some(*count);
|
||||
}
|
||||
FilterMode::Bytes(Signum::Positive(count)) if count > &1 => {
|
||||
|
||||
@@ -115,7 +115,8 @@ pub fn uu_app() -> Command {
|
||||
.long(options::APPEND)
|
||||
.short('a')
|
||||
.help(translate!("tee-help-append"))
|
||||
.action(ArgAction::SetTrue),
|
||||
.action(ArgAction::SetTrue)
|
||||
.overrides_with(options::APPEND),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::IGNORE_INTERRUPTS)
|
||||
|
||||
@@ -210,7 +210,11 @@ fn catch_sigterm() {
|
||||
/// Report that a signal is being sent if the verbose flag is set.
|
||||
fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
|
||||
if verbose {
|
||||
let s = signal_name_by_value(signal).unwrap();
|
||||
let s = if signal == 0 {
|
||||
"0".to_string()
|
||||
} else {
|
||||
signal_name_by_value(signal).unwrap().to_string()
|
||||
};
|
||||
show_error!(
|
||||
"{}",
|
||||
translate!("timeout-verbose-sending-signal", "signal" => s, "command" => cmd.quote())
|
||||
|
||||
@@ -30,6 +30,45 @@ pub fn init_collator(opts: CollatorOptions) {
|
||||
.expect("Collator already initialized");
|
||||
}
|
||||
|
||||
/// Initialize the collator for locale-aware string comparison if needed.
|
||||
///
|
||||
/// This function checks if the current locale requires locale-aware collation
|
||||
/// (UTF-8 encoding) and initializes the ICU collator with appropriate settings
|
||||
/// if necessary. For C/POSIX locales, no initialization is needed as byte
|
||||
/// comparison is sufficient.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if the collator was initialized for a UTF-8 locale, `false` if
|
||||
/// using C/POSIX locale (no initialization needed).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use uucore::i18n::collator::init_locale_collation;
|
||||
///
|
||||
/// if init_locale_collation() {
|
||||
/// // Using locale-aware collation
|
||||
/// } else {
|
||||
/// // Using byte comparison (C/POSIX locale)
|
||||
/// }
|
||||
/// ```
|
||||
pub fn init_locale_collation() -> bool {
|
||||
use crate::i18n::{UEncoding, get_locale_encoding};
|
||||
|
||||
// Check if we need locale-aware collation
|
||||
if get_locale_encoding() != UEncoding::Utf8 {
|
||||
// C/POSIX locale - no collator needed
|
||||
return false;
|
||||
}
|
||||
|
||||
// UTF-8 locale - initialize collator with Shifted mode to match GNU behavior
|
||||
let mut opts = CollatorOptions::default();
|
||||
opts.alternate_handling = Some(AlternateHandling::Shifted);
|
||||
|
||||
try_init_collator(opts)
|
||||
}
|
||||
|
||||
/// Compare both strings with regard to the current locale.
|
||||
pub fn locale_cmp(left: &[u8], right: &[u8]) -> Ordering {
|
||||
// If the detected locale is 'C', just do byte-wise comparison
|
||||
|
||||
@@ -20,7 +20,9 @@ pub enum UEncoding {
|
||||
Utf8,
|
||||
}
|
||||
|
||||
const DEFAULT_LOCALE: Locale = locale!("en-US-posix");
|
||||
// Use "und" (undefined) as the marker for C/POSIX locale
|
||||
// This ensures real locales like "en-US" won't match
|
||||
const DEFAULT_LOCALE: Locale = locale!("und");
|
||||
|
||||
/// Look at 3 environment variables in the following order
|
||||
///
|
||||
@@ -38,6 +40,11 @@ fn get_locale_from_env(locale_name: &str) -> (Locale, UEncoding) {
|
||||
let mut split = locale_var_str.split(&['.', '@']);
|
||||
|
||||
if let Some(simple) = split.next() {
|
||||
// Handle explicit C and POSIX locales - these should always use byte comparison
|
||||
if simple == "C" || simple == "POSIX" {
|
||||
return (DEFAULT_LOCALE, UEncoding::Ascii);
|
||||
}
|
||||
|
||||
// Naively convert the locale name to BCP47 tag format.
|
||||
//
|
||||
// See https://en.wikipedia.org/wiki/IETF_language_tag
|
||||
|
||||
@@ -465,11 +465,16 @@ mod tests {
|
||||
.flat_map(Teletype::try_from)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(
|
||||
pid_entry.tty(),
|
||||
Vec::from_iter(result.into_iter()).first().unwrap().clone()
|
||||
);
|
||||
// In CI environments or when running without a terminal, there may be no TTY
|
||||
if result.is_empty() {
|
||||
assert_eq!(pid_entry.tty(), Teletype::Unknown);
|
||||
} else {
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(
|
||||
pid_entry.tty(),
|
||||
Vec::from_iter(result.into_iter()).first().unwrap().clone()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -67,13 +67,11 @@ pub fn getpid() -> pid_t {
|
||||
/// so some system such as redox doesn't supported.
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
pub fn getsid(pid: i32) -> Result<pid_t, Errno> {
|
||||
unsafe {
|
||||
let result = libc::getsid(pid);
|
||||
if Errno::last() == Errno::UnknownErrno {
|
||||
Ok(result)
|
||||
} else {
|
||||
Err(Errno::last())
|
||||
}
|
||||
let result = unsafe { libc::getsid(pid) };
|
||||
if result == -1 {
|
||||
Err(Errno::last())
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -457,13 +457,13 @@ pub unsafe extern "C" fn capture_sigpipe_state() {
|
||||
#[cfg(unix)]
|
||||
macro_rules! init_sigpipe_capture {
|
||||
() => {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[used]
|
||||
#[unsafe(link_section = ".init_array")]
|
||||
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
|
||||
$crate::signals::capture_sigpipe_state;
|
||||
|
||||
#[cfg(all(unix, target_os = "macos"))]
|
||||
#[cfg(target_os = "macos")]
|
||||
#[used]
|
||||
#[unsafe(link_section = "__DATA,__mod_init_func")]
|
||||
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
|
||||
|
||||
@@ -55,8 +55,10 @@
|
||||
// spell-checker:ignore uioerror rustdoc
|
||||
|
||||
use std::{
|
||||
cell::Cell,
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
io::Write,
|
||||
sync::atomic::{AtomicI32, Ordering},
|
||||
};
|
||||
|
||||
@@ -700,6 +702,7 @@ impl From<i32> for Box<dyn UError> {
|
||||
pub struct ClapErrorWrapper {
|
||||
code: i32,
|
||||
error: clap::Error,
|
||||
print_failed: Cell<bool>,
|
||||
}
|
||||
|
||||
/// Extension trait for `clap::Error` to adjust the exit code.
|
||||
@@ -710,13 +713,21 @@ pub trait UClapError<T> {
|
||||
|
||||
impl From<clap::Error> for Box<dyn UError> {
|
||||
fn from(e: clap::Error) -> Self {
|
||||
Box::new(ClapErrorWrapper { code: 1, error: e })
|
||||
Box::new(ClapErrorWrapper {
|
||||
code: 1,
|
||||
error: e,
|
||||
print_failed: Cell::new(false),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl UClapError<ClapErrorWrapper> for clap::Error {
|
||||
fn with_exit_code(self, code: i32) -> ClapErrorWrapper {
|
||||
ClapErrorWrapper { code, error: self }
|
||||
ClapErrorWrapper {
|
||||
code,
|
||||
error: self,
|
||||
print_failed: Cell::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,12 +742,11 @@ impl UClapError<Result<clap::ArgMatches, ClapErrorWrapper>>
|
||||
impl UError for ClapErrorWrapper {
|
||||
fn code(&self) -> i32 {
|
||||
// If the error is a DisplayHelp or DisplayVersion variant,
|
||||
// we don't want to apply the custom error code, but leave
|
||||
// it 0.
|
||||
// check if printing failed. If it did, return 1, otherwise 0.
|
||||
if let clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion =
|
||||
self.error.kind()
|
||||
{
|
||||
0
|
||||
i32::from(self.print_failed.get())
|
||||
} else {
|
||||
self.code
|
||||
}
|
||||
@@ -748,9 +758,20 @@ impl Error for ClapErrorWrapper {}
|
||||
// This is abuse of the Display trait
|
||||
impl Display for ClapErrorWrapper {
|
||||
fn fmt(&self, _f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
// Intentionally ignore the result - error.print() writes directly to stderr
|
||||
// and we always return Ok(()) to satisfy Display's contract
|
||||
let _ = self.error.print();
|
||||
// Check if printing succeeds. For DisplayHelp and DisplayVersion,
|
||||
// error.print() writes to stdout, so we need to detect write failures
|
||||
// (e.g., when stdout is /dev/full).
|
||||
if let Err(print_fail) = self.error.print() {
|
||||
// Mark that printing failed so code() can return the appropriate exit code
|
||||
self.print_failed.set(true);
|
||||
// Try to display this error to stderr, but ignore if that fails too
|
||||
// since we're already in an error state.
|
||||
let _ = writeln!(std::io::stderr(), "{}: {print_fail}", crate::util_name());
|
||||
// Mirror GNU behavior: when failing to print help or version, exit with error code.
|
||||
// This avoids silent failures when stdout is full or closed.
|
||||
set_exit_code(1);
|
||||
}
|
||||
// Always return Ok(()) to satisfy Display's contract and prevent panic
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,6 +864,22 @@ fn test_write_error_handling() {
|
||||
.stderr_contains("No space left on device");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_version_help_dev_full() {
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
for option in ["--version", "--help"] {
|
||||
let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();
|
||||
|
||||
new_ucmd!()
|
||||
.arg(option)
|
||||
.set_stdout(dev_full)
|
||||
.fails()
|
||||
.stderr_contains("No space left on device");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cat_eintr_handling() {
|
||||
// Test that cat properly handles EINTR (ErrorKind::Interrupted) during I/O operations
|
||||
|
||||
@@ -648,3 +648,42 @@ fn test_comm_eintr_handling() {
|
||||
.stdout_contains("line2")
|
||||
.stdout_contains("line3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn test_comm_anonymous_pipes() {
|
||||
use std::{io::Write, os::fd::AsRawFd, process};
|
||||
use uucore::pipes::pipe;
|
||||
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
||||
// Open two anonymous pipes
|
||||
let (comm1_reader, mut comm1_writer) = pipe().unwrap();
|
||||
let (comm2_reader, mut comm2_writer) = pipe().unwrap();
|
||||
|
||||
// comm reads the data in chunks
|
||||
// make content large enough, so that at least two chunks are read
|
||||
// default buffer size is 8192, so with 6 characters (5 digits + \n) per line we need to write at least 1366 lines
|
||||
|
||||
// write 1500 lines into comm1: 00000\n00001\n...01500\n
|
||||
let mut content = String::new();
|
||||
for i in 0..1500 {
|
||||
content.push_str(&format!("{i:05}\n"));
|
||||
}
|
||||
assert!(comm1_writer.write_all(content.as_bytes()).is_ok());
|
||||
drop(comm1_writer);
|
||||
|
||||
// write into comm2: 00000\n00001\n...01500\n99999\n
|
||||
content.push_str("99999\n");
|
||||
assert!(comm2_writer.write_all(content.as_bytes()).is_ok());
|
||||
drop(comm2_writer);
|
||||
|
||||
// run comm, showing unique lines in second input
|
||||
let comm1_fd = format!("/proc/{}/fd/{}", process::id(), comm1_reader.as_raw_fd());
|
||||
let comm2_fd = format!("/proc/{}/fd/{}", process::id(), comm2_reader.as_raw_fd());
|
||||
scene
|
||||
.ucmd()
|
||||
.args(&["-13", &comm1_fd, &comm2_fd])
|
||||
.succeeds()
|
||||
.stdout_is("99999\n");
|
||||
}
|
||||
|
||||
@@ -2991,11 +2991,15 @@ fn test_copy_through_dangling_symlink() {
|
||||
fn test_copy_through_dangling_symlink_posixly_correct() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
at.touch("file");
|
||||
at.write("file", "content");
|
||||
at.symlink_file("nonexistent", "target");
|
||||
ucmd.arg("file")
|
||||
.arg("target")
|
||||
.env("POSIXLY_CORRECT", "1")
|
||||
.succeeds();
|
||||
assert!(at.file_exists("nonexistent"));
|
||||
let contents = at.read("nonexistent");
|
||||
assert_eq!(contents, "content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1469,3 +1469,31 @@ fn test_date_posix_format_specifiers() {
|
||||
.stdout_is(format!("{expected}\n"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that %x format specifier respects locale settings
|
||||
/// This is a regression test for locale-aware date formatting
|
||||
#[test]
|
||||
#[ignore = "https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2137410"]
|
||||
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
|
||||
fn test_date_format_x_locale_aware() {
|
||||
// With C locale, %x should output MM/DD/YY (US format)
|
||||
new_ucmd!()
|
||||
.env("TZ", "UTC")
|
||||
.env("LC_ALL", "C")
|
||||
.arg("-d")
|
||||
.arg("1997-01-19 08:17:48")
|
||||
.arg("+%x")
|
||||
.succeeds()
|
||||
.stdout_is("01/19/97\n");
|
||||
|
||||
// With French locale, %x should output DD/MM/YYYY (European format)
|
||||
// GNU date outputs: 19/01/1997
|
||||
new_ucmd!()
|
||||
.env("TZ", "UTC")
|
||||
.env("LC_ALL", "fr_FR.UTF-8")
|
||||
.arg("-d")
|
||||
.arg("1997-01-19 08:17:48")
|
||||
.arg("+%x")
|
||||
.succeeds()
|
||||
.stdout_is("19/01/1997\n");
|
||||
}
|
||||
|
||||
@@ -1655,6 +1655,8 @@ fn test_reading_partial_blocks_from_fifo() {
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.env("LC_ALL", "C")
|
||||
.env("LANG", "C")
|
||||
.env("LANGUAGE", "C")
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
@@ -1700,6 +1702,8 @@ fn test_reading_partial_blocks_from_fifo_unbuffered() {
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.env("LC_ALL", "C")
|
||||
.env("LANG", "C")
|
||||
.env("LANGUAGE", "C")
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
@@ -1814,6 +1818,29 @@ fn test_wrong_number_err_msg() {
|
||||
.stderr_contains("dd: invalid number: '1kBb555'\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_no_dropped_writes() {
|
||||
use std::process::Stdio;
|
||||
|
||||
const BLK_SIZE: usize = 0x4000;
|
||||
const COUNT: usize = 1000;
|
||||
const NUM_BYTES: usize = BLK_SIZE * COUNT;
|
||||
|
||||
let result = new_ucmd!()
|
||||
.args(&[
|
||||
"if=/dev/urandom",
|
||||
&format!("bs={BLK_SIZE}"),
|
||||
&format!("count={COUNT}"),
|
||||
])
|
||||
.set_stdout(Stdio::piped())
|
||||
.set_stderr(Stdio::piped())
|
||||
.succeeds();
|
||||
|
||||
assert_eq!(result.stdout().len(), NUM_BYTES);
|
||||
assert!(result.stderr_str().contains(&format!("{NUM_BYTES} bytes")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn test_oflag_direct_partial_block() {
|
||||
|
||||
@@ -9,6 +9,11 @@ fn test_invalid_arg() {
|
||||
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_operand() {
|
||||
new_ucmd!().fails_with_code(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_with_trailing_slashes() {
|
||||
new_ucmd!()
|
||||
@@ -71,15 +76,11 @@ fn test_dirname_non_utf8_paths() {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
// Create a test file with non-UTF-8 bytes in the name
|
||||
let non_utf8_bytes = b"test_\xFF\xFE/file.txt";
|
||||
let non_utf8_name = OsStr::from_bytes(non_utf8_bytes);
|
||||
|
||||
// Test that dirname handles non-UTF-8 paths without crashing
|
||||
let result = new_ucmd!().arg(non_utf8_name).succeeds();
|
||||
|
||||
// Just verify it didn't crash and produced some output
|
||||
// The exact output format may vary due to lossy conversion
|
||||
let output = result.stdout_str_lossy();
|
||||
assert!(!output.is_empty());
|
||||
assert!(output.contains("test_"));
|
||||
@@ -105,8 +106,6 @@ fn test_emoji_handling() {
|
||||
|
||||
#[test]
|
||||
fn test_trailing_dot() {
|
||||
// Basic case: path ending with /. should return parent without stripping last component
|
||||
// This matches GNU coreutils behavior and fixes issue #8910
|
||||
new_ucmd!()
|
||||
.arg("/home/dos/.")
|
||||
.succeeds()
|
||||
@@ -156,7 +155,7 @@ fn test_trailing_dot_edge_cases() {
|
||||
new_ucmd!()
|
||||
.arg("/home/dos//.")
|
||||
.succeeds()
|
||||
.stdout_is("/home/dos/\n");
|
||||
.stdout_is("/home/dos\n");
|
||||
|
||||
// Path with . in middle (should use normal logic)
|
||||
new_ucmd!()
|
||||
@@ -182,26 +181,19 @@ fn test_trailing_dot_non_utf8() {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
// Create a path with non-UTF-8 bytes ending in /.
|
||||
let non_utf8_bytes = b"/test_\xFF\xFE/.";
|
||||
let non_utf8_path = OsStr::from_bytes(non_utf8_bytes);
|
||||
|
||||
// Test that dirname handles non-UTF-8 paths with /. suffix
|
||||
let result = new_ucmd!().arg(non_utf8_path).succeeds();
|
||||
|
||||
// The output should be the path without the /. suffix
|
||||
let output = result.stdout_str_lossy();
|
||||
assert!(!output.is_empty());
|
||||
assert!(output.contains("test_"));
|
||||
// Should not contain the . at the end
|
||||
assert!(!output.trim().ends_with('.'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_existing_behavior_preserved() {
|
||||
// Ensure we didn't break existing test cases
|
||||
// These tests verify backward compatibility
|
||||
|
||||
// Normal paths without /. should work as before
|
||||
new_ucmd!().arg("/home/dos").succeeds().stdout_is("/home\n");
|
||||
|
||||
@@ -216,3 +208,56 @@ fn test_existing_behavior_preserved() {
|
||||
.succeeds()
|
||||
.stdout_is("/home/dos\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_paths_comprehensive() {
|
||||
// Comprehensive test for multiple paths in single invocation
|
||||
new_ucmd!()
|
||||
.args(&[
|
||||
"/home/dos/.",
|
||||
"/var/log",
|
||||
".",
|
||||
"/tmp/.",
|
||||
"",
|
||||
"/",
|
||||
"relative/path",
|
||||
])
|
||||
.succeeds()
|
||||
.stdout_is("/home/dos\n/var\n.\n/tmp\n.\n/\nrelative\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_dot_slash_variations() {
|
||||
new_ucmd!().arg("foo//.").succeeds().stdout_is("foo\n");
|
||||
|
||||
new_ucmd!().arg("foo///.").succeeds().stdout_is("foo\n");
|
||||
|
||||
new_ucmd!().arg("foo/./").succeeds().stdout_is("foo\n");
|
||||
|
||||
new_ucmd!()
|
||||
.arg("foo/bar/./")
|
||||
.succeeds()
|
||||
.stdout_is("foo/bar\n");
|
||||
|
||||
new_ucmd!().arg("foo/./bar").succeeds().stdout_is("foo/.\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dot_slash_component_preservation() {
|
||||
new_ucmd!().arg("a/./b").succeeds().stdout_is("a/.\n");
|
||||
|
||||
new_ucmd!()
|
||||
.arg("a/./b/./c")
|
||||
.succeeds()
|
||||
.stdout_is("a/./b/.\n");
|
||||
|
||||
new_ucmd!()
|
||||
.arg("foo/./bar/baz")
|
||||
.succeeds()
|
||||
.stdout_is("foo/./bar\n");
|
||||
|
||||
new_ucmd!()
|
||||
.arg("/path/./to/file")
|
||||
.succeeds()
|
||||
.stdout_is("/path/./to\n");
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
//! # CI Integration
|
||||
//! EINTR handling tests are NOW visible in CI logs through integration tests:
|
||||
//! - `test_cat_eintr_handling` in `tests/by-util/test_cat.rs`
|
||||
//! - `test_comm_eintr_handling` in `tests/by-util/test_comm.rs`
|
||||
//! - `test_comm_eintr_handling` in `tests/by-util/test_comm.rs`
|
||||
//! - `test_od_eintr_handling` in `tests/by-util/test_od.rs`
|
||||
//!
|
||||
//!
|
||||
//! These integration tests use the mock utilities from this module to verify
|
||||
//! that each utility properly handles signal interruptions during I/O operations.
|
||||
//! Test results appear in CI logs under the "Test" steps when running `cargo nextest run`.
|
||||
@@ -171,7 +171,7 @@ mod tests {
|
||||
assert_eq!(n, 5);
|
||||
assert_eq!(&buf, b"hello");
|
||||
|
||||
// Read rest of data without interruption
|
||||
// Read rest of data without interruption
|
||||
let n = reader.read(&mut buf).unwrap();
|
||||
assert_eq!(n, 5);
|
||||
assert_eq!(&buf, b" worl"); // Second chunk of "hello world"
|
||||
|
||||
@@ -2545,3 +2545,25 @@ fn test_install_unprivileged_option_u_skips_chown() {
|
||||
assert!(at.file_exists(dst_ok));
|
||||
assert_eq!(at.metadata(dst_ok).uid(), geteuid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_install_normal_file_replaces_symlink() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
|
||||
at.write("source", "new content");
|
||||
at.write("sensitive", "important data");
|
||||
|
||||
// Create symlink at destination
|
||||
at.symlink_file("sensitive", "dest");
|
||||
|
||||
// Install should replace symlink with normal file (not follow it)
|
||||
scene.ucmd().arg("source").arg("dest").succeeds();
|
||||
|
||||
// Verify dest is now a normal file, not a symlink
|
||||
assert!(at.file_exists("dest"));
|
||||
assert_eq!(at.read("dest"), "new content");
|
||||
|
||||
// Verify sensitive file was NOT modified
|
||||
assert_eq!(at.read("sensitive"), "important data");
|
||||
}
|
||||
|
||||
@@ -4951,6 +4951,36 @@ fn test_dereference_symlink_file_color() {
|
||||
.stdout_is(out_exp);
|
||||
}
|
||||
|
||||
/// Symlink chain target should be colored by final target type, not as symlink (#8934).
|
||||
#[test]
|
||||
fn test_symlink_chain_target_color() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
at.touch("file");
|
||||
at.relative_symlink_file("file", "link1");
|
||||
at.relative_symlink_file("link1", "link2");
|
||||
let out = ucmd
|
||||
.args(&["-l", "--color=always", "link2"])
|
||||
.succeeds()
|
||||
.stdout_move_str();
|
||||
let target = out.split("->").nth(1).unwrap();
|
||||
assert!(!target.contains("36m")); // 36m = cyan (symlink color)
|
||||
}
|
||||
|
||||
/// Symlink target should be colored by extension (e.g., .tar.gz shows as archive color).
|
||||
#[test]
|
||||
fn test_symlink_target_extension_color() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
at.touch("archive.tar.gz");
|
||||
at.relative_symlink_file("archive.tar.gz", "link");
|
||||
let out = ucmd
|
||||
.env("LS_COLORS", "*.tar.gz=31")
|
||||
.args(&["-l", "--color=always", "link"])
|
||||
.succeeds()
|
||||
.stdout_move_str();
|
||||
let target = out.split("->").nth(1).unwrap();
|
||||
assert!(target.contains("31m")); // 31 = red (our configured archive color)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tabsize_option() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
||||
@@ -1217,3 +1217,20 @@ fn test_progress_no_output_on_error() {
|
||||
.stderr_contains("cannot remove")
|
||||
.stderr_contains("No such file or directory");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_symlink_to_readonly_no_prompt() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
at.touch("foo");
|
||||
at.set_mode("foo", 0o444);
|
||||
at.symlink_file("foo", "bar");
|
||||
|
||||
ucmd.arg("---presume-input-tty")
|
||||
.arg("bar")
|
||||
.succeeds()
|
||||
.no_stderr();
|
||||
|
||||
assert!(!at.symlink_exists("bar"));
|
||||
}
|
||||
|
||||
@@ -2463,4 +2463,58 @@ fn test_start_buffer() {
|
||||
.stdout_only_bytes(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_locale_collation_c_locale() {
|
||||
// C locale uses byte order - this is deterministic and tests the fix for #9148
|
||||
// Accented characters (UTF-8 multibyte) sort after ASCII letters
|
||||
let input = "é\ne\nE\na\nA\nz\n";
|
||||
// C locale byte order: A=0x41, E=0x45, a=0x61, e=0x65, z=0x7A, é=0xC3 0xA9
|
||||
let expected = "A\nE\na\ne\nz\né\n";
|
||||
|
||||
new_ucmd!()
|
||||
.env("LC_ALL", "C")
|
||||
.pipe_in(input)
|
||||
.succeeds()
|
||||
.stdout_is(expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_locale_collation_utf8() {
|
||||
// Test French UTF-8 locale handling - behavior depends on i18n-collator feature
|
||||
// With feature: locale-aware collation (é sorts near e)
|
||||
// Without feature: byte order (é after z, since 0xC3A9 > 0x7A)
|
||||
let input = "z\né\ne\na\n";
|
||||
|
||||
let result = new_ucmd!()
|
||||
.env("LC_ALL", "fr_FR.UTF-8")
|
||||
.pipe_in(input)
|
||||
.succeeds();
|
||||
|
||||
let output = result.stdout_str();
|
||||
let lines: Vec<&str> = output.lines().collect();
|
||||
|
||||
assert_eq!(lines.len(), 4, "Expected 4 sorted lines");
|
||||
assert_eq!(lines[0], "a", "'a' (0x61) should always sort first");
|
||||
|
||||
// Validate based on which collation mode is active
|
||||
if lines[3] == "é" {
|
||||
// Byte order mode: é (0xC3A9) > z (0x7A)
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec!["a", "e", "z", "é"],
|
||||
"Byte order mode: expected a < e < z < é"
|
||||
);
|
||||
} else {
|
||||
// Locale collation mode: é sorts with base letter e
|
||||
assert_eq!(lines[3], "z", "Locale mode: 'z' should sort last");
|
||||
let z_pos = lines.iter().position(|&x| x == "z").unwrap();
|
||||
let e_pos = lines.iter().position(|&x| x == "e").unwrap();
|
||||
let e_accent_pos = lines.iter().position(|&x| x == "é").unwrap();
|
||||
assert!(
|
||||
e_pos < z_pos && e_accent_pos < z_pos,
|
||||
"Locale mode: 'e' ({e_pos}) and 'é' ({e_accent_pos}) should sort before 'z' ({z_pos})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* spell-checker: enable */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore dyld dylib setvbuf
|
||||
// spell-checker:ignore cmdline dyld dylib PDEATHSIG setvbuf
|
||||
#[cfg(target_os = "linux")]
|
||||
use uutests::at_and_ucmd;
|
||||
use uutests::new_ucmd;
|
||||
@@ -276,3 +276,72 @@ fn test_stdbuf_non_utf8_paths() {
|
||||
.succeeds()
|
||||
.stdout_is("test content for stdbuf\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_stdbuf_no_fork_regression() {
|
||||
// Regression test for issue #9066: https://github.com/uutils/coreutils/issues/9066
|
||||
// The original stdbuf implementation used fork+spawn which broke signal handling
|
||||
// and PR_SET_PDEATHSIG. This test verifies that stdbuf uses exec() instead.
|
||||
// With fork: stdbuf process would remain visible in process list
|
||||
// With exec: stdbuf process is replaced by target command (GNU compatible)
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
||||
// Start stdbuf with a long-running command
|
||||
let mut child = Command::new(&scene.bin_path)
|
||||
.args(["stdbuf", "-o0", "sleep", "3"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("Failed to start stdbuf");
|
||||
|
||||
let child_pid = child.id();
|
||||
|
||||
// Poll until exec happens or timeout
|
||||
let cmdline_path = format!("/proc/{child_pid}/cmdline");
|
||||
let timeout = Duration::from_secs(2);
|
||||
let poll_interval = Duration::from_millis(10);
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let command_name = loop {
|
||||
if start_time.elapsed() > timeout {
|
||||
child.kill().ok();
|
||||
panic!("TIMEOUT: Process {child_pid} did not respond within {timeout:?}");
|
||||
}
|
||||
|
||||
if let Ok(cmdline) = std::fs::read_to_string(&cmdline_path) {
|
||||
let cmd_parts: Vec<&str> = cmdline.split('\0').collect();
|
||||
let name = cmd_parts.first().map_or("", |v| v);
|
||||
|
||||
// Wait for exec to complete (process name changes from original binary to target)
|
||||
// Handle both multicall binary (coreutils) and individual utilities (stdbuf)
|
||||
if !name.contains("coreutils") && !name.contains("stdbuf") && !name.is_empty() {
|
||||
break name.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(poll_interval);
|
||||
};
|
||||
|
||||
// The loop already waited for exec (no longer original binary), so this should always pass
|
||||
// But keep the assertion as a safety check and clear documentation
|
||||
assert!(
|
||||
!command_name.contains("coreutils") && !command_name.contains("stdbuf"),
|
||||
"REGRESSION: Process {child_pid} is still original binary (coreutils or stdbuf) - fork() used instead of exec()"
|
||||
);
|
||||
|
||||
// Ensure we're running the expected target command
|
||||
assert!(
|
||||
command_name.contains("sleep"),
|
||||
"Expected 'sleep' command at PID {child_pid}, got: {command_name}"
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
child.kill().ok();
|
||||
child.wait().ok();
|
||||
}
|
||||
|
||||
@@ -5040,6 +5040,22 @@ fn tail_n_lines_with_emoji() {
|
||||
.stdout_only("💐\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tail_bytes_exceeds_file_size() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
// Should be > 4096 bytes (block size can vary):
|
||||
at.write("test_file.txt", &"x".repeat(5000));
|
||||
|
||||
ts.ucmd()
|
||||
.arg("-c")
|
||||
.arg("1048576")
|
||||
.arg("test_file.txt")
|
||||
.succeeds()
|
||||
.stdout_only("x".repeat(5000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_follow_pipe_f() {
|
||||
|
||||
@@ -91,6 +91,30 @@ fn test_tee_append() {
|
||||
assert_eq!(at.read(file), content.repeat(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tee_multiple_append_flags() {
|
||||
// Test for bug: https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2134578
|
||||
// The command should accept multiple -a flags for different files
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
let content = "don't fail me now rust";
|
||||
let file1 = "log1";
|
||||
let file2 = "log2";
|
||||
|
||||
// Pre-populate files with some content to verify append behavior
|
||||
at.write(file1, "existing1\n");
|
||||
at.write(file2, "existing2\n");
|
||||
|
||||
ucmd.args(&["-a", file1, "-a", file2])
|
||||
.pipe_in(content)
|
||||
.succeeds()
|
||||
.stdout_is(content);
|
||||
|
||||
assert!(at.file_exists(file1));
|
||||
assert!(at.file_exists(file2));
|
||||
assert_eq!(at.read(file1), format!("existing1\n{content}"));
|
||||
assert_eq!(at.read(file2), format!("existing2\n{content}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readonly() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
@@ -58,7 +58,7 @@ fn test_verbose() {
|
||||
new_ucmd!()
|
||||
.args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "1"])
|
||||
.fails()
|
||||
.stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n");
|
||||
.stderr_only("timeout: sending signal 0 to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+70
-69
@@ -9,15 +9,16 @@
|
||||
set -e
|
||||
|
||||
# Use GNU make, readlink and sed on *BSD and macOS
|
||||
MAKE=$(command -v gmake||command -v make)
|
||||
READLINK=$(command -v greadlink||command -v readlink) # Use our readlink to remove a dependency
|
||||
SED=$(command -v gsed||command -v sed)
|
||||
command -v gmake && make(){ gmake "$@";}
|
||||
command -v greadlink && readlink(){ greadlink "$@";} # todo: use our readlink for less deps
|
||||
command -v gsed && sed(){ gsed "$@";}
|
||||
SED=$(command -v gsed||command -v sed) # for find...exec...
|
||||
|
||||
SYSTEM_TIMEOUT=$(command -v timeout)
|
||||
SYSTEM_YES=$(command -v yes)
|
||||
|
||||
ME="${0}"
|
||||
ME_dir="$(dirname -- "$("${READLINK}" -fm -- "${ME}")")"
|
||||
ME_dir="$(dirname -- "$(readlink -fm -- "${ME}")")"
|
||||
REPO_main_dir="$(dirname -- "${ME_dir}")"
|
||||
|
||||
|
||||
@@ -28,7 +29,7 @@ unset CARGOFLAGS
|
||||
### * config (from environment with fallback defaults); note: GNU is expected to be a sibling repo directory
|
||||
|
||||
path_UUTILS=${path_UUTILS:-${REPO_main_dir}}
|
||||
path_GNU="$("${READLINK}" -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")"
|
||||
path_GNU="$(readlink -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")"
|
||||
|
||||
###
|
||||
|
||||
@@ -90,15 +91,15 @@ cd -
|
||||
export CARGOFLAGS # tell to make
|
||||
if [ "${SELINUX_ENABLED}" = 1 ];then
|
||||
# Build few utils for SELinux for faster build. MULTICALL=y fails...
|
||||
"${MAKE}" UTILS="cat chcon chmod cp cut dd echo env groups id install ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon seq stat test touch tr true uname wc whoami"
|
||||
make UTILS="cat chcon chmod cp cut dd echo env groups id install ln ls mkdir mkfifo mknod mktemp mv printf rm rmdir runcon seq stat test touch tr true uname wc whoami"
|
||||
else
|
||||
# Use MULTICALL=y for faster build
|
||||
"${MAKE}" MULTICALL=y SKIP_UTILS=more
|
||||
make MULTICALL=y SKIP_UTILS=more
|
||||
for binary in $("${UU_BUILD_DIR}"/coreutils --list)
|
||||
do [ -e "${UU_BUILD_DIR}/${binary}" ] || ln -vf "${UU_BUILD_DIR}/coreutils" "${UU_BUILD_DIR}/${binary}"
|
||||
done
|
||||
fi
|
||||
[ -e "${UU_BUILD_DIR}/ginstall" ] || ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use renamed install to ginstall
|
||||
[ -e "${UU_BUILD_DIR}/ginstall" ] || ln -vf "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests use ginstall
|
||||
##
|
||||
|
||||
cd "${path_GNU}" && echo "[ pwd:'${PWD}' ]"
|
||||
@@ -112,7 +113,7 @@ done
|
||||
|
||||
# Always update the PATH to test the uutils coreutils instead of the GNU coreutils
|
||||
# This ensures the correct path is used even if the repository was moved or rebuilt in a different location
|
||||
"${SED}" -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk
|
||||
sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk
|
||||
|
||||
if test -f gnu-built; then
|
||||
echo "GNU build already found. Skip"
|
||||
@@ -120,7 +121,7 @@ if test -f gnu-built; then
|
||||
echo "Note: the customization of the tests will still happen"
|
||||
else
|
||||
# Disable useless checks
|
||||
"${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk
|
||||
sed -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk
|
||||
# Stop manpage generation for cleaner log
|
||||
: > man/local.mk
|
||||
# Use CFLAGS for best build time since we discard GNU coreutils
|
||||
@@ -128,15 +129,15 @@ else
|
||||
--enable-single-binary=symlinks --enable-install-program="arch,kill,uptime,hostname" \
|
||||
"$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)"
|
||||
#Add timeout to to protect against hangs
|
||||
"${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver
|
||||
sed -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver
|
||||
# Use a better diff
|
||||
"${SED}" -i 's|diff -c|diff -u|g' tests/Coreutils.pm
|
||||
sed -i 's|diff -c|diff -u|g' tests/Coreutils.pm
|
||||
|
||||
# Skip make if possible
|
||||
# Use GNU nproc for *BSD and macOS
|
||||
NPROC="$(command -v nproc||command -v gnproc)"
|
||||
test "${SELINUX_ENABLED}" = 1 && touch src/getlimits # SELinux tests does not use it
|
||||
test -f src/getlimits || "${MAKE}" -j "$("${NPROC}")"
|
||||
test -f src/getlimits || make -j "$("${NPROC}")"
|
||||
cp -f src/getlimits "${UU_BUILD_DIR}"
|
||||
|
||||
# Handle generated factor tests
|
||||
@@ -151,12 +152,12 @@ else
|
||||
)
|
||||
for i in ${seq}; do
|
||||
echo "strip t${i}.sh from Makefile"
|
||||
"${SED}" -i -e "s/\$(tf)\/t${i}.sh//g" Makefile
|
||||
sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile
|
||||
done
|
||||
|
||||
# Remove tests checking for --version & --help
|
||||
# Not really interesting for us and logs are too big
|
||||
"${SED}" -i -e '/tests\/help\/help-version.sh/ D' \
|
||||
sed -i -e '/tests\/help\/help-version.sh/ D' \
|
||||
-e '/tests\/help\/help-version-getopt.sh/ D' \
|
||||
Makefile
|
||||
touch gnu-built
|
||||
@@ -167,36 +168,36 @@ grep -rl 'path_prepend_' tests/* | xargs -r "${SED}" -i 's| path_prepend_ ./src|
|
||||
grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r "${SED}" -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g"
|
||||
|
||||
# We can't build runcon and chcon without libselinux. But GNU no longer builds dummies of them. So consider they are SELinux specific.
|
||||
"${SED}" -i 's/^print_ver_.*/require_selinux_/' tests/runcon/runcon-compute.sh
|
||||
"${SED}" -i 's/^print_ver_.*/require_selinux_/' tests/runcon/runcon-no-reorder.sh
|
||||
"${SED}" -i 's/^print_ver_.*/require_selinux_/' tests/chcon/chcon-fail.sh
|
||||
sed -i 's/^print_ver_.*/require_selinux_/' tests/runcon/runcon-compute.sh
|
||||
sed -i 's/^print_ver_.*/require_selinux_/' tests/runcon/runcon-no-reorder.sh
|
||||
sed -i 's/^print_ver_.*/require_selinux_/' tests/chcon/chcon-fail.sh
|
||||
|
||||
# Mask mtab by unshare instead of LD_PRELOAD (able to merge this to GNU?)
|
||||
"${SED}" -i -e 's|^export LD_PRELOAD=.*||' -e "s|.*maybe LD_PRELOAD.*|df() { unshare -rm bash -c \"mount -t tmpfs tmpfs /proc \&\& command df \\\\\"\\\\\$@\\\\\"\" -- \"\$@\"; }|" tests/df/no-mtab-status.sh
|
||||
sed -i -e 's|^export LD_PRELOAD=.*||' -e "s|.*maybe LD_PRELOAD.*|df() { unshare -rm bash -c \"mount -t tmpfs tmpfs /proc \&\& command df \\\\\"\\\\\$@\\\\\"\" -- \"\$@\"; }|" tests/df/no-mtab-status.sh
|
||||
# We use coreutils yes
|
||||
"${SED}" -i "s|--coreutils-prog=||g" tests/misc/coreutils.sh
|
||||
sed -i "s|--coreutils-prog=||g" tests/misc/coreutils.sh
|
||||
# Different message
|
||||
"${SED}" -i "s|coreutils: unknown program 'blah'|blah: function/utility not found|" tests/misc/coreutils.sh
|
||||
sed -i "s|coreutils: unknown program 'blah'|blah: function/utility not found|" tests/misc/coreutils.sh
|
||||
|
||||
# Use the system coreutils where the test fails due to error in a util that is not the one being tested
|
||||
"${SED}" -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh
|
||||
sed -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh
|
||||
|
||||
# our messages are better
|
||||
"${SED}" -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh
|
||||
"${SED}" -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh
|
||||
sed -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh
|
||||
sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh
|
||||
|
||||
# Our message is a bit better
|
||||
"${SED}" -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh
|
||||
sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh
|
||||
|
||||
# Our message is better
|
||||
"${SED}" -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl
|
||||
sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl
|
||||
|
||||
"${SED}" -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh
|
||||
sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh
|
||||
|
||||
# trap_sigpipe_or_skip_ fails with uutils tools because of a bug in
|
||||
# timeout/yes (https://github.com/uutils/coreutils/issues/7252), so we use
|
||||
# system's yes/timeout to make sure the tests run (instead of being skipped).
|
||||
"${SED}" -i 's|\(trap .* \)timeout\( .* \)yes|'"\1${SYSTEM_TIMEOUT}\2${SYSTEM_YES}"'|' init.cfg
|
||||
sed -i 's|\(trap .* \)timeout\( .* \)yes|'"\1${SYSTEM_TIMEOUT}\2${SYSTEM_YES}"'|' init.cfg
|
||||
|
||||
# Remove dup of /usr/bin/ and /usr/local/bin/ when executed several times
|
||||
grep -rlE '/usr/bin/\s?/usr/bin' init.cfg tests/* | xargs -r "${SED}" -Ei 's|/usr/bin/\s?/usr/bin/|/usr/bin/|g'
|
||||
@@ -207,101 +208,101 @@ grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r "${SED}
|
||||
# we should not regress our project just to match what GNU is going.
|
||||
# So, do some changes on the fly
|
||||
|
||||
"${SED}" -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh
|
||||
sed -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh
|
||||
|
||||
# 'rel' doesn't exist. Our implementation is giving a better message.
|
||||
"${SED}" -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh
|
||||
sed -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh
|
||||
|
||||
# Our implementation shows "Directory not empty" for directories that can't be accessed due to lack of execute permissions
|
||||
# This is actually more accurate than "Permission denied" since the real issue is that we can't empty the directory
|
||||
"${SED}" -i -e "s|rm: cannot remove 'a/1': Permission denied|rm: cannot remove 'a/1/2': Permission denied|g" -e "s|rm: cannot remove 'b': Permission denied|rm: cannot remove 'a': Directory not empty\nrm: cannot remove 'b/3': Permission denied|g" tests/rm/rm2.sh
|
||||
sed -i -e "s|rm: cannot remove 'a/1': Permission denied|rm: cannot remove 'a/1/2': Permission denied|g" -e "s|rm: cannot remove 'b': Permission denied|rm: cannot remove 'a': Directory not empty\nrm: cannot remove 'b/3': Permission denied|g" tests/rm/rm2.sh
|
||||
|
||||
# overlay-headers.sh test intends to check for inotify events,
|
||||
# however there's a bug because `---dis` is an alias for: `---disable-inotify`
|
||||
sed -i -e "s|---dis ||g" tests/tail/overlay-headers.sh
|
||||
|
||||
# Do not FAIL, just do a regular ERROR
|
||||
"${SED}" -i -e "s|framework_failure_ 'no inotify_add_watch';|fail=1;|" tests/tail/inotify-rotate-resources.sh
|
||||
sed -i -e "s|framework_failure_ 'no inotify_add_watch';|fail=1;|" tests/tail/inotify-rotate-resources.sh
|
||||
|
||||
# pr-tests.pl: Override the comparison function to suppress diff output
|
||||
# This prevents the test from overwhelming logs while still reporting failures
|
||||
"${SED}" -i '/^my $fail = run_tests/i no warnings "redefine"; *Coreutils::_compare_files = sub { my ($p, $t, $io, $a, $e) = @_; my $d = File::Compare::compare($a, $e); warn "$p: test $t: mismatch\\n" if $d; return $d; };' tests/pr/pr-tests.pl
|
||||
sed -i '/^my $fail = run_tests/i no warnings "redefine"; *Coreutils::_compare_files = sub { my ($p, $t, $io, $a, $e) = @_; my $d = File::Compare::compare($a, $e); warn "$p: test $t: mismatch\\n" if $d; return $d; };' tests/pr/pr-tests.pl
|
||||
|
||||
# We don't have the same error message and no need to be that specific
|
||||
"${SED}" -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \
|
||||
sed -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \
|
||||
-e "s|--pages argument '\$too_big' too large|invalid --pages argument '\$too_big'|" \
|
||||
-e "s|invalid page range|invalid --pages argument|" tests/misc/xstrtol.pl
|
||||
|
||||
# When decoding an invalid base32/64 string, gnu writes everything it was able to decode until
|
||||
# it hit the decode error, while we don't write anything if the input is invalid.
|
||||
"${SED}" -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/basenc/base64.pl
|
||||
"${SED}" -i "s/\(\(b2[ml]_[69]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl
|
||||
sed -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/basenc/base64.pl
|
||||
sed -i "s/\(\(b2[ml]_[69]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl
|
||||
|
||||
# add "error: " to the expected error message
|
||||
"${SED}" -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl
|
||||
sed -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl
|
||||
|
||||
# basenc: swap out error message for unexpected arg
|
||||
"${SED}" -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: unexpected argument '--foobar' found\n\n tip: to pass '--foobar' as a value, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information, try '--help'.\n\"}]/" tests/basenc/basenc.pl
|
||||
"${SED}" -i "s/ {ERR_SUBST=>\"s\/(unrecognized|unknown) option \[-' \]\*foobar\[' \]\*\/foobar\/\"}],//" tests/basenc/basenc.pl
|
||||
sed -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: unexpected argument '--foobar' found\n\n tip: to pass '--foobar' as a value, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information, try '--help'.\n\"}]/" tests/basenc/basenc.pl
|
||||
sed -i "s/ {ERR_SUBST=>\"s\/(unrecognized|unknown) option \[-' \]\*foobar\[' \]\*\/foobar\/\"}],//" tests/basenc/basenc.pl
|
||||
|
||||
# exit early for the selinux check. The first is enough for us.
|
||||
"${SED}" -i "s|# Independent of whether SELinux|return 0\n #|g" init.cfg
|
||||
sed -i "s|# Independent of whether SELinux|return 0\n #|g" init.cfg
|
||||
|
||||
# Some tests are executed with the "nobody" user.
|
||||
# The check to verify if it works is based on the GNU coreutils version
|
||||
# making it too restrictive for us
|
||||
"${SED}" -i "s|\$PACKAGE_VERSION|[0-9]*|g" tests/rm/fail-2eperm.sh tests/mv/sticky-to-xpart.sh init.cfg
|
||||
sed -i "s|\$PACKAGE_VERSION|[0-9]*|g" tests/rm/fail-2eperm.sh tests/mv/sticky-to-xpart.sh init.cfg
|
||||
|
||||
# usage_vs_getopt.sh is heavily modified as it runs all the binaries
|
||||
# with the option -/ is used, clap is returning a better error than GNU's. Adjust the GNU test
|
||||
"${SED}" -i -e "s~ grep \" '\*/'\*\" err || framework_failure_~ grep \" '*-/'*\" err || framework_failure_~" tests/misc/usage_vs_getopt.sh
|
||||
"${SED}" -i -e "s~ sed -n \"1s/'\\\/'/'OPT'/p\" < err >> pat || framework_failure_~ sed -n \"1s/'-\\\/'/'OPT'/p\" < err >> pat || framework_failure_~" tests/misc/usage_vs_getopt.sh
|
||||
sed -i -e "s~ grep \" '\*/'\*\" err || framework_failure_~ grep \" '*-/'*\" err || framework_failure_~" tests/misc/usage_vs_getopt.sh
|
||||
sed -i -e "s~ sed -n \"1s/'\\\/'/'OPT'/p\" < err >> pat || framework_failure_~ sed -n \"1s/'-\\\/'/'OPT'/p\" < err >> pat || framework_failure_~" tests/misc/usage_vs_getopt.sh
|
||||
# Ignore runcon, it needs some extra attention
|
||||
# For all other tools, we want drop-in compatibility, and that includes the exit code.
|
||||
"${SED}" -i -e "s/rcexp=1$/rcexp=1\n case \"\$prg\" in runcon|stdbuf) return;; esac/" tests/misc/usage_vs_getopt.sh
|
||||
sed -i -e "s/rcexp=1$/rcexp=1\n case \"\$prg\" in runcon|stdbuf) return;; esac/" tests/misc/usage_vs_getopt.sh
|
||||
# GNU has option=[SUFFIX], clap is <SUFFIX>
|
||||
"${SED}" -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getopt.sh
|
||||
sed -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getopt.sh
|
||||
# for some reasons, some stuff are duplicated, strip that
|
||||
"${SED}" -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh
|
||||
sed -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh
|
||||
|
||||
# install verbose messages shows ginstall as command
|
||||
"${SED}" -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh
|
||||
sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh
|
||||
|
||||
# GNU doesn't support padding < -LONG_MAX
|
||||
# disable this test case
|
||||
"${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/numfmt/numfmt.pl
|
||||
sed -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/numfmt/numfmt.pl
|
||||
|
||||
# Update the GNU error message to match the one generated by clap
|
||||
"${SED}" -i -e "s/\$prog: multiple field specifications/error: the argument '--field <FIELDS>' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/numfmt/numfmt.pl
|
||||
"${SED}" -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n <files>...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '<files>...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh
|
||||
sed -i -e "s/\$prog: multiple field specifications/error: the argument '--field <FIELDS>' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/numfmt/numfmt.pl
|
||||
sed -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n <files>...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '<files>...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh
|
||||
|
||||
# our error message is better
|
||||
"${SED}" -i -e "s|mv: cannot overwrite 'a/t': Directory not empty|mv: cannot move 'b/t' to 'a/t': Directory not empty|" tests/mv/dir2dir.sh
|
||||
sed -i -e "s|mv: cannot overwrite 'a/t': Directory not empty|mv: cannot move 'b/t' to 'a/t': Directory not empty|" tests/mv/dir2dir.sh
|
||||
|
||||
# GNU doesn't support width > INT_MAX
|
||||
# disable these test cases
|
||||
"${SED}" -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl
|
||||
sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl
|
||||
|
||||
"${SED}" -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold <SIZE>' but none was supplied/" -e "s/Try 'du --help' for more information./\nFor more information, try '--help'./" tests/du/threshold.sh
|
||||
sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold <SIZE>' but none was supplied/" -e "s/Try 'du --help' for more information./\nFor more information, try '--help'./" tests/du/threshold.sh
|
||||
|
||||
# Remove the extra output check
|
||||
"${SED}" -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl
|
||||
"${SED}" -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl
|
||||
sed -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl
|
||||
sed -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl
|
||||
|
||||
# Skip the move-dir-while-traversing test - our implementation uses safe traversal with openat()
|
||||
# which avoids the TOCTOU race condition that this test tries to trigger. The test uses inotify
|
||||
# to detect when du opens a directory path and moves it to cause an error, but our openat-based
|
||||
# implementation doesn't trigger inotify events on the full path, preventing the race condition.
|
||||
# This is actually better behavior - we're immune to this class of filesystem race attacks.
|
||||
"${SED}" -i '1s/^/exit 0 # Skip test - uutils du uses safe traversal that prevents this race condition\n/' tests/du/move-dir-while-traversing.sh
|
||||
sed -i '1s/^/exit 0 # Skip test - uutils du uses safe traversal that prevents this race condition\n/' tests/du/move-dir-while-traversing.sh
|
||||
|
||||
awk 'BEGIN {count=0} /compare exp out2/ && count < 6 {sub(/compare exp out2/, "grep -q \"cannot be used with\" out2"); count++} 1' tests/df/df-output.sh > tests/df/df-output.sh.tmp && mv tests/df/df-output.sh.tmp tests/df/df-output.sh
|
||||
|
||||
# with ls --dired, in case of error, we have a slightly different error position
|
||||
"${SED}" -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh
|
||||
sed -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh
|
||||
|
||||
# small difference in the error message
|
||||
"${SED}" -i -e "s/ls: invalid argument 'XX' for 'time style'/ls: invalid --time-style argument 'XX'/" \
|
||||
sed -i -e "s/ls: invalid argument 'XX' for 'time style'/ls: invalid --time-style argument 'XX'/" \
|
||||
-e "s/Valid arguments are:/Possible values are:/" \
|
||||
-e "s/Try 'ls --help' for more information./\nFor more information try --help/" \
|
||||
tests/ls/time-style-diag.sh
|
||||
@@ -309,29 +310,29 @@ awk 'BEGIN {count=0} /compare exp out2/ && count < 6 {sub(/compare exp out2/, "g
|
||||
# disable two kind of tests:
|
||||
# "hostid BEFORE --help" doesn't fail for GNU. we fail. we are probably doing better
|
||||
# "hostid BEFORE --help AFTER " same for this
|
||||
"${SED}" -i -e "s/env \$prog \$BEFORE \$opt > out2/env \$prog \$BEFORE \$opt > out2 #/" -e "s/env \$prog \$BEFORE \$opt AFTER > out3/env \$prog \$BEFORE \$opt AFTER > out3 #/" -e "s/compare exp out2/compare exp out2 #/" -e "s/compare exp out3/compare exp out3 #/" tests/help/help-version-getopt.sh
|
||||
sed -i -e "s/env \$prog \$BEFORE \$opt > out2/env \$prog \$BEFORE \$opt > out2 #/" -e "s/env \$prog \$BEFORE \$opt AFTER > out3/env \$prog \$BEFORE \$opt AFTER > out3 #/" -e "s/compare exp out2/compare exp out2 #/" -e "s/compare exp out3/compare exp out3 #/" tests/help/help-version-getopt.sh
|
||||
|
||||
# Add debug info + we have less syscall then GNU's. Adjust our check.
|
||||
"${SED}" -i -e '/test \$n_stat1 = \$n_stat2 \\/c\
|
||||
sed -i -e '/test \$n_stat1 = \$n_stat2 \\/c\
|
||||
echo "n_stat1 = \$n_stat1"\n\
|
||||
echo "n_stat2 = \$n_stat2"\n\
|
||||
test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh
|
||||
|
||||
# no need to replicate this output with hashsum
|
||||
"${SED}" -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl
|
||||
sed -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl
|
||||
|
||||
# Our ls command always outputs ANSI color codes prepended with a zero. However,
|
||||
# in the case of GNU, it seems inconsistent. Nevertheless, it looks like it
|
||||
# doesn't matter whether we prepend a zero or not.
|
||||
"${SED}" -i -E 's/\^\[\[([1-9]m)/^[[0\1/g; s/\^\[\[m/^[[0m/g' tests/ls/color-norm.sh
|
||||
sed -i -E 's/\^\[\[([1-9]m)/^[[0\1/g; s/\^\[\[m/^[[0m/g' tests/ls/color-norm.sh
|
||||
# It says in the test itself that having more than one reset is a bug, so we
|
||||
# don't need to replicate that behavior.
|
||||
"${SED}" -i -E 's/(\^\[\[0m)+/\^\[\[0m/g' tests/ls/color-norm.sh
|
||||
sed -i -E 's/(\^\[\[0m)+/\^\[\[0m/g' tests/ls/color-norm.sh
|
||||
|
||||
# GNU's ls seems to output color codes in the order given in the environment
|
||||
# variable, but our ls seems to output them in a predefined order. Nevertheless,
|
||||
# the order doesn't matter, so it's okay.
|
||||
"${SED}" -i 's/44;37/37;44/' tests/ls/multihardlink.sh
|
||||
sed -i 's/44;37/37;44/' tests/ls/multihardlink.sh
|
||||
|
||||
# Just like mentioned in the previous patch, GNU's ls output color codes in the
|
||||
# same way it is specified in the environment variable, but our ls emits them
|
||||
@@ -340,19 +341,19 @@ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh
|
||||
# individually, for example, ^[[31^[[42 instead of ^[[31;42, but we don't do
|
||||
# that anywhere in our implementation, and it looks like GNU's ls also doesn't
|
||||
# do that. So, it's okay to ignore the zero.
|
||||
"${SED}" -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/color-clear-to-eol.sh
|
||||
sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/color-clear-to-eol.sh
|
||||
|
||||
# patching this because of the same reason as the last one.
|
||||
"${SED}" -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/quote-align.sh
|
||||
sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/quote-align.sh
|
||||
|
||||
# Slightly different error message
|
||||
"${SED}" -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh
|
||||
sed -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh
|
||||
|
||||
# upstream doesn't having the program name in the error message
|
||||
# but we do. We should keep it that way.
|
||||
"${SED}" -i 's/echo "changing security context/echo "chcon: changing security context/' tests/chcon/chcon.sh
|
||||
sed -i 's/echo "changing security context/echo "chcon: changing security context/' tests/chcon/chcon.sh
|
||||
|
||||
# Disable this test, it is not relevant for us:
|
||||
# * the selinux crate is handling errors
|
||||
# * the test says "maybe we should not fail when no context available"
|
||||
"${SED}" -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh
|
||||
sed -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
set -e
|
||||
# Treat unset variables as errors
|
||||
set -u
|
||||
# Ensure pipeline failures are caught (not just the last command's exit code)
|
||||
set -o pipefail
|
||||
# Print expanded commands to stdout before running them
|
||||
set -x
|
||||
|
||||
@@ -39,7 +41,12 @@ REPO_main_dir="$(dirname -- "${ME_dir}")"
|
||||
FEATURES_OPTION=${FEATURES_OPTION:-"--features=feat_os_unix"}
|
||||
COVERAGE_DIR=${COVERAGE_DIR:-"${REPO_main_dir}/coverage"}
|
||||
|
||||
LLVM_PROFDATA="$(find "$(rustc --print sysroot)" -name llvm-profdata)"
|
||||
# Find llvm-profdata in the nightly toolchain (which is used for coverage builds)
|
||||
LLVM_PROFDATA="$(find "$(RUSTUP_TOOLCHAIN=nightly-gnu rustc --print sysroot)" -name llvm-profdata)"
|
||||
if [ -z "${LLVM_PROFDATA}" ]; then
|
||||
echo "Error: llvm-profdata not found. Install it with: rustup +nightly-gnu component add llvm-tools"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROFRAW_DIR="${COVERAGE_DIR}/traces"
|
||||
PROFDATA_DIR="${COVERAGE_DIR}/data"
|
||||
|
||||
+2
-1
@@ -16,4 +16,5 @@ curl -L ${repo}/raw/refs/heads/master/tests/stty/bad-speed.sh > tests/stty/bad-s
|
||||
curl -L ${repo}/raw/refs/heads/master/tests/runcon/runcon-compute.sh > tests/runcon/runcon-compute.sh
|
||||
curl -L ${repo}/raw/refs/heads/master/tests/tac/tac-continue.sh > tests/tac/tac-continue.sh
|
||||
# Add tac-continue.sh to root tests (it requires root to mount tmpfs)
|
||||
sed -i 's|tests/split/l-chunk-root.sh.*|tests/split/l-chunk-root.sh\t\t\t\\\n tests/tac/tac-continue.sh\t\t\t\\|' tests/local.mk
|
||||
# Use sed -i.bak for macOS
|
||||
sed -i.bak 's|tests/split/l-chunk-root.sh.*|tests/split/l-chunk-root.sh\t\t\t\\\n tests/tac/tac-continue.sh\t\t\t\\|' tests/local.mk
|
||||
|
||||
Reference in New Issue
Block a user