From e93e0b3b2122a2b56c5d8d5445bb14bfec3c7a15 Mon Sep 17 00:00:00 2001 From: sap1110 <238814652+sap1110@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:47:38 +0530 Subject: [PATCH] numfmt: reject numbers with too many digits A number whose whole part or fraction has more than 33 digits, not counting leading zeros, is now refused with "value too large to be converted" instead of being silently rounded, matching GNU numfmt. Closes #12855 --- src/uu/numfmt/locales/en-US.ftl | 1 + src/uu/numfmt/locales/fr-FR.ftl | 1 + src/uu/numfmt/src/diagnostics.rs | 10 +- src/uu/numfmt/src/format.rs | 29 ++++++ tests/by-util/test_numfmt.rs | 154 +++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 3 deletions(-) diff --git a/src/uu/numfmt/locales/en-US.ftl b/src/uu/numfmt/locales/en-US.ftl index bc6d6fc3dde..28b1062355d 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 a7c55d6d981..55a6eb2d457 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 d73dad4aa70..84f1790cfd2 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 ee0a1dd1329..cc6ba38154e 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 56d127d4726..121bc6e6f9b 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() {