From 268c26e58acf20173c690f8b87a19b476d03ebdf Mon Sep 17 00:00:00 2001 From: vibhuthespeedst Date: Mon, 26 Oct 2026 08:02:41 +0530 Subject: [PATCH] uucore: version_cmp: compare endings when stems tie as versions Endings were only compared when the stems were byte-identical, so tool-1.00.txt and tool-1.0.exe compared Equal and sort -V -u dropped one of them. Fixes #15795 --- src/uucore/src/lib/features/version_cmp.rs | 28 +++++++++++++++++++++- tests/by-util/test_sort.rs | 9 +++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/uucore/src/lib/features/version_cmp.rs b/src/uucore/src/lib/features/version_cmp.rs index d19484d38d3..6cd7e1f9555 100644 --- a/src/uucore/src/lib/features/version_cmp.rs +++ b/src/uucore/src/lib/features/version_cmp.rs @@ -103,8 +103,13 @@ pub fn version_cmp(mut a: &[u8], mut b: &[u8]) -> Ordering { // Try to strip file extensions let (mut a, mut b) = match (remove_file_ending(a), remove_file_ending(b)) { - (a_stripped, b_stripped) if a_stripped == b_stripped => { + (a_stripped, b_stripped) + if a_stripped == b_stripped + || ((a_stripped, b_stripped) != (a, b) + && version_cmp(a_stripped, b_stripped) == Ordering::Equal) => + { // If both would be the same after stripping file extensions, don't strip them. + // "Same" means equal as versions, not only as bytes (`1.00` and `1.0`). (a, b) } stripped => stripped, @@ -368,4 +373,25 @@ mod tests { "NULL bytes are handled comparison" ); } + + #[test] + fn test_version_cmp_endings_decide_version_equal_stems() { + // The stems only differ in leading zeros, so they tie as versions and the + // endings must decide. Equal here makes `sort -V -u` drop one of the lines. + for (a, b) in [ + (&b"tool-1.00.txt"[..], &b"tool-1.0.exe"[..]), + (b"tool-1.00", b"tool-1.0.exe"), + (b"01.txt", b"1.log"), + (b"setup0.txt", b"setup.exe"), + (b"lib-2.010.so", b"lib-2.10.a"), + (b"app-1.00.zip", b"app-1.0.tar.gz"), + ] { + assert_ne!(version_cmp(a, b), Ordering::Equal); + assert_eq!(version_cmp(a, b), version_cmp(b, a).reverse()); + } + assert_eq!( + version_cmp(b"tool-1.00.txt", b"tool-1.0.exe"), + Ordering::Greater + ); + } } diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index c309ad5b29e..cea2c4dd2d6 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -302,6 +302,15 @@ fn test_version_sort_unstable() { .stdout_is("0.1\n0.002\n0.02\n0.2\n0.3\n"); } +#[test] +fn test_version_sort_unique_keeps_different_endings() { + new_ucmd!() + .args(&["-V", "-u"]) + .pipe_in("tool-1.00.txt\ntool-1.0.exe\n") + .succeeds() + .stdout_is("tool-1.0.exe\ntool-1.00.txt\n"); +} + #[test] fn test_version_sort_stable() { new_ucmd!()