mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07: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 }
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user