Merge branch 'main' into core-freepfx

This commit is contained in:
oech3
2026-01-18 14:39:52 +09:00
49 changed files with 987 additions and 427 deletions
+31 -23
View File
@@ -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
+8 -9
View File
@@ -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
+1 -1
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
+1
View File
@@ -549,6 +549,7 @@ uutests.workspace = true
uucore = { workspace = true, features = [
"mode",
"entries",
"pipes",
"process",
"signals",
"utmpx",
+3 -2
View File
@@ -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>
+5
View File
@@ -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)?;
+18 -13
View File
@@ -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
View File
@@ -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 })
+85 -53
View File
@@ -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}");
}
+33 -9
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -40
View File
@@ -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
View File
@@ -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,
+84 -60
View File
@@ -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
View File
@@ -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()
),
+4
View File
@@ -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 }

Some files were not shown because too many files have changed in this diff Show More