Skip to content

numfmt panics (str char-boundary) on a number using a multibyte locale decimal separator before a multibyte char and a suffix #13937

Description

@leeewee

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions