diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 9c03801ef7d..a8cbe319625 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()); + // What follows the number begins where the number ends, which is a + // position in bytes: the decimal separator of a locale such as ar-SA is + // two bytes wide, so skipping that many *characters* instead lands past + // the suffix, and the byte index taken from it lands inside a character. + let mut characters = s[numeric_part.len()..].chars(); let potential_suffix = characters.next(); let potential_i = characters.next(); @@ -65,14 +69,12 @@ fn find_valid_number_with_suffix(s: &str, unit: Unit) -> Option<&str> { return Some(numeric_part); } + // Every suffix is one ASCII character, so the byte it ends on is known. 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(suffix), Some(_)) if RawSuffix::try_from(&suffix).is_ok() => { + (Some(suffix), _) if RawSuffix::try_from(&suffix).is_ok() => { Some(&s[..=numeric_part.len()]) } _ => Some(numeric_part), diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 4db86944771..e5921ce0cf3 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1614,6 +1614,35 @@ fn test_locale_fr_rejects_period() { .stderr_contains("invalid"); } +/// The suffix begins where the number ends, and ar-SA's decimal separator is +/// two bytes wide, so counting that end in characters walked into the middle +/// of the one after it and aborted the process. +#[test] +#[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")] +fn test_locale_multibyte_separator_before_a_multibyte_char() { + for (unit, input) in [ + ("si", "1٫€K"), + ("si", "1٫€Kx"), + ("auto", "1٫€Ki"), + ("iec-i", "1٫€K"), + ("none", "1٫€K"), + ] { + new_ucmd!() + .env("LC_ALL", "ar_SA.UTF-8") + .args(&[format!("--from={unit}"), input.into()]) + .fails_with_code(2) + .stderr_contains("invalid suffix in input"); + } + + // The suffix that is there is still found: '€' is what is wrong with this + // one, not 'K'. + new_ucmd!() + .env("LC_ALL", "ar_SA.UTF-8") + .args(&["--from=si", "1٫Ki"]) + .fails_with_code(2) + .stderr_is("numfmt: invalid suffix in input '1٫Ki': 'i'\n"); +} + #[test] fn test_locale_c_uses_period() { // C locale should still use '.' as usual