diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 9c03801ef7d..ab41906e20e 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -57,7 +57,9 @@ fn find_valid_number_with_suffix(s: &str, unit: Unit) -> Option<&str> { let accepts_suffix = unit != Unit::None; let accepts_i = [Unit::Auto, Unit::Iec(true)].contains(&unit); - let mut characters = s.chars().skip(numeric_part.len()); + // Slice by byte length, not `.chars().skip()`: the numeric part may + // contain a multi-byte decimal separator (e.g. Arabic ٫). + let mut characters = s[numeric_part.len()..].chars(); let potential_suffix = characters.next(); let potential_i = characters.next(); @@ -66,14 +68,11 @@ fn find_valid_number_with_suffix(s: &str, unit: Unit) -> Option<&str> { } match (potential_suffix, potential_i) { - (Some(suffix), None) if RawSuffix::try_from(&suffix).is_ok() => { - Some(&s[..=numeric_part.len()]) - } (Some(suffix), Some('i')) if accepts_i && RawSuffix::try_from(&suffix).is_ok() => { - Some(&s[..numeric_part.len() + 2]) + Some(&s[..numeric_part.len() + suffix.len_utf8() + 'i'.len_utf8()]) } - (Some(suffix), Some(_)) if RawSuffix::try_from(&suffix).is_ok() => { - Some(&s[..=numeric_part.len()]) + (Some(suffix), _) if RawSuffix::try_from(&suffix).is_ok() => { + Some(&s[..numeric_part.len() + suffix.len_utf8()]) } _ => Some(numeric_part), } diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 4db86944771..b64cf206074 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1614,6 +1614,28 @@ fn test_locale_fr_rejects_period() { .stderr_contains("invalid"); } +#[test] +#[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")] +fn test_locale_multibyte_separator_does_not_panic_on_invalid_suffix() { + // Regression test for #13937: under a locale whose decimal separator is + // multi-byte (Arabic ٫, 2 bytes), an invalid multi-byte suffix character + // right after the numeric part used to abort the process instead of + // being cleanly rejected, since the suffix search skipped by the + // separator's *byte* length as though it were a *character* count. + for arg in ["1٫€K", "1٫€Kx"] { + new_ucmd!() + .env("LC_ALL", "ar_SA.UTF-8") + .args(&["--from=si", arg]) + .fails() + .stderr_contains("invalid suffix"); + } + new_ucmd!() + .env("LC_ALL", "ar_SA.UTF-8") + .args(&["--from=auto", "1٫€Ki"]) + .fails() + .stderr_contains("invalid suffix"); +} + #[test] fn test_locale_c_uses_period() { // C locale should still use '.' as usual