Under a locale whose decimal separator is multibyte (e.g. LC_ALL=ar_SA.UTF-8, where the separator is the Arabic ٫ U+066B, 2 bytes), numfmt aborts on an input like 1٫€K.
Steps to reproduce
$ LC_ALL=ar_SA.UTF-8 numfmt --from=si '1٫€K'
thread 'main' panicked at src/uu/numfmt/src/format.rs:70:20:
byte index 4 is not a char boundary; it is inside '€' (bytes 3..6) of `1٫€K`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted (core dumped)
$ echo $?
134
The three match arms of find_valid_number_with_suffix each abort on a matching shape (all under LC_ALL=ar_SA.UTF-8):
| input |
site |
arm |
numfmt --from=si '1٫€K' |
format.rs:70 |
(Some(suffix), None) |
numfmt --from=si '1٫€Kx' |
format.rs:76 |
(Some(suffix), Some(_)) |
numfmt --from=auto '1٫€Ki' |
format.rs:73 |
(Some(suffix), Some('i')) |
GNU behavior
GNU rejects the malformed input and exit non-zero without crashing.
$ LC_ALL=ar_SA.UTF-8 /usr/bin/numfmt --from=si '1٫€K'
numfmt: invalid suffix in input: '1٫€K'
$ echo $?
2
Root cause
|
// finds the valid beginning part of an input string, or None. |
|
fn find_valid_number_with_suffix(s: &str, unit: Unit) -> Option<&str> { |
|
let numeric_part = find_numeric_beginning(s)?; |
|
|
|
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()); |
|
let potential_suffix = characters.next(); |
|
let potential_i = characters.next(); |
|
|
|
if !accepts_suffix { |
|
return Some(numeric_part); |
|
} |
|
|
|
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(&s[..=numeric_part.len()]) |
|
} |
|
_ => Some(numeric_part), |
|
} |
|
} |
find_valid_number_with_suffix computes numeric_part.len() (a byte length) and then uses it both as a chars().skip(...) char count and as a byte-slice index &s[..=numeric_part.len()]. When the numeric part contains the multibyte separator, those two accountings desync: the char-skip lands on a later valid suffix, but the byte-slice cuts into the multibyte € mid-character, so the str slice panics (byte index N is not a char boundary) — panic=abort → SIGABRT, exit 134.
Under a locale whose decimal separator is multibyte (e.g.
LC_ALL=ar_SA.UTF-8, where the separator is the Arabic٫U+066B, 2 bytes),numfmtaborts on an input like1٫€K.Steps to reproduce
The three match arms of
find_valid_number_with_suffixeach abort on a matching shape (all underLC_ALL=ar_SA.UTF-8):numfmt --from=si '1٫€K'format.rs:70(Some(suffix), None)numfmt --from=si '1٫€Kx'format.rs:76(Some(suffix), Some(_))numfmt --from=auto '1٫€Ki'format.rs:73(Some(suffix), Some('i'))GNU behavior
GNU rejects the malformed input and exit non-zero without crashing.
Root cause
coreutils/src/uu/numfmt/src/format.rs
Lines 53 to 80 in ebaf6e9
find_valid_number_with_suffixcomputesnumeric_part.len()(a byte length) and then uses it both as achars().skip(...)char count and as a byte-slice index&s[..=numeric_part.len()]. When the numeric part contains the multibyte separator, those two accountings desync: the char-skip lands on a later valid suffix, but the byte-slice cuts into the multibyte€mid-character, so thestrslice panics (byte index N is not a char boundary) —panic=abort→ SIGABRT, exit 134.