Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/uu/numfmt/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ numfmt-error-rejecting-suffix = rejecting suffix in input: '{ $number }{ $suffix
numfmt-error-suffix-unsupported-for-unit = This suffix is unsupported for specified unit
numfmt-error-invalid-unit-argument = invalid argument '{$arg}' for '{$opt}'
numfmt-error-number-too-big = Number is too big and unsupported
numfmt-error-too-many-digits = value too large to be converted: { $input }
numfmt-error-format-no-percent = format { $format } has no % directive
numfmt-error-format-ends-in-percent = format { $format } ends in %
numfmt-error-invalid-format-directive = invalid format { $format }, directive must be %[0]['][-][N][.][N]f
Expand Down
1 change: 1 addition & 0 deletions src/uu/numfmt/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ numfmt-error-rejecting-suffix = rejet du suffixe dans l'entrée : '{ $number }{
numfmt-error-suffix-unsupported-for-unit = Ce suffixe n'est pas pris en charge pour l'unité spécifiée
numfmt-error-unit-auto-not-supported-with-to = L'unité 'auto' n'est pas prise en charge avec les options --to
numfmt-error-number-too-big = Le nombre est trop grand et non pris en charge
numfmt-error-too-many-digits = valeur trop grande pour être convertie : { $input }
numfmt-error-format-no-percent = le format { $format } n'a pas de directive %
numfmt-error-format-ends-in-percent = le format { $format } se termine par %
numfmt-error-invalid-format-directive = format invalide { $format }, la directive doit être %[0]['][-][N][.][N]f
Expand Down
10 changes: 7 additions & 3 deletions src/uu/numfmt/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use uucore::diagnostics::{Snapshot, ValueOptions};
use uucore::ranges::RangeError;
use uucore::translate;

use crate::format::{holds_number, invalid_span};
use crate::format::{holds_number, invalid_span, overlong_number_len};
use crate::options::{FormatError, FormatErrorKind, NumfmtOptions, OptionValueError};
use crate::units::Unit;

Expand Down Expand Up @@ -97,12 +97,16 @@ pub fn render_input(
else {
return false;
};
let span = invalid_span(input, options);
// A number with too many digits is refused as a whole, so it is what the
// caret marks, and no suffix advice would help.
let start = input.len() - input.trim_start().len();
let overlong = overlong_number_len(&input[start..]);
let span = overlong.map_or_else(|| invalid_span(input, options), |len| start..start + len);
// Without --from there is no suffix to spell out, only the option to reach
// for; an input with no number in it at all is not a suffix question, and
// neither is one whose leading part only looks like the start of one.
let help = match options.transform.from {
_ if !holds_number(input) => None,
_ if overlong.is_some() || !holds_number(input) => None,
Unit::None => Some("numfmt-diag-help-input-no-from"),
_ => Some("numfmt-diag-help-input-suffixes"),
};
Expand Down
29 changes: 29 additions & 0 deletions src/uu/numfmt/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,30 @@ pub fn holds_number(input: &str) -> bool {
})
}

/// How many digits numfmt converts on either side of the decimal separator,
/// as GNU does. Leading zeros carry no value and are not counted.
const MAX_DIGITS: usize = 33;

/// Byte length of the number `s` starts with, when its whole part or its
/// fraction has more than [`MAX_DIGITS`] digits; `None` otherwise.
pub fn overlong_number_len(s: &str) -> Option<usize> {
let dec_sep = locale_decimal_separator();
let digits_len = |s: &str| s.bytes().take_while(u8::is_ascii_digit).count();
let counted = |digits: &str| digits.trim_start_matches('0').len();

let sign_len = usize::from(s.starts_with('-'));
let whole_len = digits_len(&s[sign_len..]);
let whole = &s[sign_len..sign_len + whole_len];
let fraction = s[sign_len + whole_len..]
.strip_prefix(dec_sep)
.map(|rest| &rest[..digits_len(rest)]);

if counted(whole) <= MAX_DIGITS && fraction.is_none_or(|f| counted(f) <= MAX_DIGITS) {
return None;
}
Some(sign_len + whole_len + fraction.map_or(0, |f| dec_sep.len() + f.len()))
}

fn detailed_error_message(s: &str, unit: Unit, unit_separator: &str) -> Option<String> {
if s.is_empty() {
return Some(translate!("numfmt-error-invalid-number-empty"));
Expand Down Expand Up @@ -482,6 +506,11 @@ fn transform_from(
opts: &TransformOptions,
options: &NumfmtOptions,
) -> Result<ParsedNumber> {
// Checked first: whatever follows the digits, the number itself is refused.
if overlong_number_len(s).is_some() {
return Err(translate!("numfmt-error-too-many-digits", "input" => s.quote()));
}

let (i, suffix) = parse_suffix(
s,
opts.from,
Expand Down
154 changes: 154 additions & 0 deletions tests/by-util/test_numfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1854,6 +1854,134 @@ fn test_header_detached() {
.stdout_is("1\n2\n");
}

// https://github.com/uutils/coreutils/issues/12855
#[test]
fn test_too_many_digits_issue_input() {
let input = "1091611621123456668921298.1279298262626672676226672772762626627828";
new_ucmd!()
.arg(input)
.fails_with_code(2)
.no_stdout()
.stderr_is(format!(
"numfmt: value too large to be converted: '{input}'\n"
));
}

#[test]
fn test_too_many_digits_whole_part() {
new_ucmd!()
.args(&["--to=si", &"1".repeat(33)])
.succeeds()
.stdout_only("112Q\n");

let input = "1".repeat(34);
new_ucmd!()
.args(&["--to=si", &input])
.fails_with_code(2)
.no_stdout()
.stderr_is(format!(
"numfmt: value too large to be converted: '{input}'\n"
));
}

#[test]
fn test_too_many_digits_fraction() {
new_ucmd!()
.args(&["--to=si", &format!("1.{}", "1".repeat(33))])
.succeeds()
.stdout_only("1\n");

// Trailing zeros of the fraction count.
for input in [
format!("1.{}", "1".repeat(34)),
format!("1.1{}", "0".repeat(33)),
] {
new_ucmd!()
.args(&["--to=si", &input])
.fails_with_code(2)
.no_stdout()
.stderr_is(format!(
"numfmt: value too large to be converted: '{input}'\n"
));
}
}

#[test]
fn test_too_many_digits_leading_zeros_not_counted() {
new_ucmd!()
.args(&["--to=si", &format!("0000{}", "1".repeat(33))])
.succeeds()
.stdout_only("112Q\n");
new_ucmd!()
.args(&["--to=si", &format!("1.0000{}", "1".repeat(33))])
.succeeds()
.stdout_only("1\n");

let input = format!("0000{}", "1".repeat(34));
new_ucmd!()
.args(&["--to=si", &input])
.fails_with_code(2)
.no_stdout()
.stderr_is(format!(
"numfmt: value too large to be converted: '{input}'\n"
));
}

#[test]
fn test_too_many_digits_negative() {
new_ucmd!()
.args(&["--to=si", "--", &format!("-{}", "1".repeat(33))])
.succeeds()
.stdout_only("-112Q\n");

for input in [
format!("-{}", "1".repeat(34)),
format!("-1.{}", "1".repeat(34)),
] {
new_ucmd!()
.args(&["--to=si", "--", &input])
.fails_with_code(2)
.no_stdout()
.stderr_is(format!(
"numfmt: value too large to be converted: '{input}'\n"
));
}
}

#[test]
fn test_too_many_digits_comes_before_suffix_errors() {
for (unit, input) in [
("--from=si", format!("{}K", "1".repeat(34))),
("--from=none", format!("{}x", "1".repeat(34))),
] {
new_ucmd!()
.args(&[unit, &input])
.fails_with_code(2)
.no_stdout()
.stderr_is(format!(
"numfmt: value too large to be converted: '{input}'\n"
));
}
}

#[test]
fn test_too_many_digits_follows_invalid_mode() {
let input = "1".repeat(34);
let message = format!("numfmt: value too large to be converted: '{input}'\n");
new_ucmd!()
.args(&["--to=si", "--invalid=warn"])
.pipe_in(format!("1000\n{input}\n2000\n"))
.succeeds()
.stdout_is(format!("1.0k\n{input}\n2.0k\n"))
.stderr_is(&message);
new_ucmd!()
.args(&["--to=si", "--invalid=fail"])
.pipe_in(format!("1000\n{input}\n2000\n"))
.fails_with_code(2)
.stdout_is(format!("1.0k\n{input}\n2.0k\n"))
.stderr_is(&message);
}

#[cfg(all(feature = "feat_diagnostics", not(wasi_runner)))]
mod diagnostics {
use super::*;
Expand Down Expand Up @@ -2143,6 +2271,32 @@ numfmt: invalid number: 'abc'
);
}

#[cfg(unix)]
#[test]
fn test_snippet_marks_a_number_with_too_many_digits() {
let input = format!("{}.5K", "1".repeat(34));
let result = new_ucmd!()
.terminal_sim_stderr()
.args(&["--from=si", &input])
.fails_with_code(2);

// The number is refused whatever follows it, so the caret leaves the
// suffix out and no advice about suffixes is given.
result.no_stdout();
assert_eq!(
result.stderr_as_displayed(),
format!(
"numfmt: value too large to be converted: '{input}'
╭─[ numfmt:1:18 ]
│
1 │ numfmt --from=si {input}
│ {}
───╯",
"─".repeat(36)
)
);
}

#[cfg(unix)]
#[test]
fn test_snippet_points_at_the_i_a_unit_does_not_take() {
Expand Down
Loading