diff --git a/src/uu/numfmt/locales/en-US.ftl b/src/uu/numfmt/locales/en-US.ftl index bc6d6fc3dd..28b1062355 100644 --- a/src/uu/numfmt/locales/en-US.ftl +++ b/src/uu/numfmt/locales/en-US.ftl @@ -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 diff --git a/src/uu/numfmt/locales/fr-FR.ftl b/src/uu/numfmt/locales/fr-FR.ftl index a7c55d6d98..55a6eb2d45 100644 --- a/src/uu/numfmt/locales/fr-FR.ftl +++ b/src/uu/numfmt/locales/fr-FR.ftl @@ -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 diff --git a/src/uu/numfmt/src/diagnostics.rs b/src/uu/numfmt/src/diagnostics.rs index d73dad4aa7..84f1790cfd 100644 --- a/src/uu/numfmt/src/diagnostics.rs +++ b/src/uu/numfmt/src/diagnostics.rs @@ -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; @@ -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"), }; diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index ee0a1dd132..cc6ba38154 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -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 { + 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 { if s.is_empty() { return Some(translate!("numfmt-error-invalid-number-empty")); @@ -482,6 +506,11 @@ fn transform_from( opts: &TransformOptions, options: &NumfmtOptions, ) -> Result { + // 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, diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 56d127d472..121bc6e6f9 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -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::*; @@ -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() {