diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 9c03801ef7..deb7a045ff 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -57,7 +57,11 @@ 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()); + // Look at the characters immediately after the numeric part. `numeric_part` is a prefix of + // `s`, so its byte length is a valid char boundary to slice/iterate from. (Using it as a + // *character* count for `chars().skip(..)` was wrong for multi-byte numeric parts, e.g. a + // multi-byte decimal separator, and led to slicing inside a multi-byte suffix char. See #13937.) + let mut characters = s[numeric_part.len()..].chars(); let potential_suffix = characters.next(); let potential_i = characters.next(); @@ -66,14 +70,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 4db8694477..1b1fd6ad3f 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1614,6 +1614,20 @@ 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_invalid_suffix_does_not_panic() { + // Regression test for #13937: under a locale with a multi-byte decimal separator (Arabic + // '٫', U+066B, 2 bytes), an input like "1٫€K" made numfmt slice inside a multi-byte + // character while parsing the suffix and panic. It must instead reject the malformed suffix + // like GNU (non-zero exit, "invalid suffix in input"), without crashing. + new_ucmd!() + .env("LC_ALL", "ar_SA.UTF-8") + .args(&["--from=auto", "1٫€K"]) + .fails() + .stderr_contains("invalid suffix in input"); +} + #[test] fn test_locale_c_uses_period() { // C locale should still use '.' as usual