From cfb57bfa3aa90573abfc39a160bb0248e14fd64f Mon Sep 17 00:00:00 2001 From: Sachit Madaan Date: Sat, 19 Sep 2026 07:49:15 -0700 Subject: [PATCH] Fix printf conversion of a zero value with a precision of zero (#4942) --- include/fmt/printf.h | 30 ++++++++++++++++++++++++++++-- test/printf-test.cc | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/include/fmt/printf.h b/include/fmt/printf.h index 8d792fbd..0caeae0c 100644 --- a/include/fmt/printf.h +++ b/include/fmt/printf.h @@ -88,7 +88,7 @@ struct printf_precision_handler { auto operator()(T value) -> int { if (!int_checker::is_signed>::fits_in_int(value)) report_error("number is too big"); - return max_of(static_cast(value), 0); + return detail::is_negative(value) ? -1 : static_cast(value); } template ::value)> @@ -246,6 +246,19 @@ class printf_arg_formatter : public arg_formatter { detail::write(this->out, value, this->specs, this->locale); } + // C requires no characters when converting a zero value with a precision of + // zero, so only the sign and the padding remain. + void write_zero_with_zero_precision() { + auto s = this->specs; + const char* sign_str = s.sign() == sign::plus ? "+" + : s.sign() == sign::space ? " " + : ""; + s.set_type(presentation_type::none); + if (s.align() == align::none || s.align() == align::numeric) + s.set_align(align::right); + write_bytes(this->out, sign_str, s); + } + public: printf_arg_formatter(basic_appender iter, format_specs& s, context_type& ctx) @@ -258,6 +271,13 @@ class printf_arg_formatter : public arg_formatter { // MSVC2013 fails to compile separate overloads for bool and Char so use // std::is_same instead. if (!std::is_same::value) { + auto t = this->specs.type(); + if (value == 0 && this->specs.precision == 0 && + (t == presentation_type::dec || t == presentation_type::oct || + t == presentation_type::hex)) { + write_zero_with_zero_precision(); + return; + } write(value); return; } @@ -490,7 +510,6 @@ void vprintf(buffer& buf, basic_string_view format, str, to_unsigned(nul != str_end ? nul - str : specs.precision)); arg = sv; } - if (specs.alt() && arg.visit(is_zero_int())) specs.clear_alt(); if (specs.fill_unit() == '0') { if (is_arithmetic_type(arg.type()) && specs.align() != align::left) { specs.set_align(align::numeric); @@ -550,6 +569,13 @@ void vprintf(buffer& buf, basic_string_view format, if (specs.type() == presentation_type::none) report_error("invalid format specifier"); if (upper) specs.set_upper(); + // For '#o', C requires a single '0' when the value and the precision are + // both zero. + bool alt_zero = specs.alt() && arg.visit(is_zero_int()); + if (alt_zero) specs.clear_alt(); + if (alt_zero && specs.type() == presentation_type::oct && + specs.precision == 0) + specs.precision = 1; start = it; diff --git a/test/printf-test.cc b/test/printf-test.cc index baf2b168..01a9f632 100644 --- a/test/printf-test.cc +++ b/test/printf-test.cc @@ -278,6 +278,40 @@ TEST(printf_test, int_precision) { EXPECT_PRINTF("00042 ", "%-#10.5o", 042); } +// C99 7.21.6.1: the result of converting a zero value with a precision of zero +// is no characters. +TEST(printf_test, zero_int_with_zero_precision) { + EXPECT_PRINTF("", "%.0d", 0); + EXPECT_PRINTF("", "%.d", 0); + EXPECT_PRINTF("", "%.0i", 0); + EXPECT_PRINTF("", "%.0o", 0); + EXPECT_PRINTF("", "%.0u", 0); + EXPECT_PRINTF("", "%.0x", 0); + EXPECT_PRINTF("", "%.0X", 0); + + // '#' forces a single '0' for octal, but has no effect on other conversions. + EXPECT_PRINTF("0", "%#.0o", 0); + EXPECT_PRINTF("0", "%#.0hho", 256); + EXPECT_PRINTF("", "%#.0x", 0); + EXPECT_PRINTF("", "%#.0X", 0); + + // The sign, space and width still apply. + EXPECT_PRINTF("+", "%+.0d", 0); + EXPECT_PRINTF(" ", "% .0d", 0); + EXPECT_PRINTF(" ", "%5.0d", 0); + EXPECT_PRINTF(" ", "%-5.0d", 0); + EXPECT_PRINTF(" ", "%05.0d", 0); + EXPECT_PRINTF(" 0", "%#5.0o", 0); + EXPECT_PRINTF("0 ", "%#-5.0o", 0); + + // A nonzero value or a nonzero precision is unaffected. + EXPECT_PRINTF("42", "%.0d", 42); + EXPECT_PRINTF("-42", "%.0d", -42); + EXPECT_PRINTF("ff", "%.0x", 255); + EXPECT_PRINTF("0", "%.1d", 0); + EXPECT_PRINTF("00", "%.2d", 0); +} + TEST(printf_test, float_precision) { char buffer[256]; safe_sprintf(buffer, "%.3e", 1234.5678); @@ -300,6 +334,8 @@ TEST(printf_test, ignore_precision_for_non_numeric_arg) { TEST(printf_test, dynamic_precision) { EXPECT_EQ("00042", test_sprintf("%.*d", 5, 42)); EXPECT_EQ("42", test_sprintf("%.*d", -5, 42)); + EXPECT_EQ("0", test_sprintf("%.*d", -1, 0)); + EXPECT_EQ("+0", test_sprintf("%+.*d", -1, 0)); EXPECT_THROW_MSG(test_sprintf("%.*d", 5.0, 42), format_error, "precision is not integer"); EXPECT_THROW_MSG(test_sprintf("%.*d"), format_error, "argument not found"); @@ -326,6 +362,8 @@ TEST(printf_test, positional_width) { TEST(printf_test, positional_precision) { EXPECT_EQ("00042", test_sprintf("%2$.*1$d", 5, 42)); EXPECT_EQ("42", test_sprintf("%2$.*1$d", -5, 42)); + EXPECT_EQ("0", test_sprintf("%2$.*1$d", -1, 0)); + EXPECT_EQ("+0", test_sprintf("%2$+.*1$d", -1, 0)); EXPECT_EQ("Hell", test_sprintf("%2$.*1$s", 4, "Hello")); EXPECT_THROW_MSG(test_sprintf("%2$.*1$d", 5.0, 42), format_error, "precision is not integer"); @@ -561,7 +599,7 @@ TEST(printf_test, check_format_string_regression) { } TEST(printf_test, fixed_large_exponent) { - EXPECT_EQ("1000000000000000000000", fmt::sprintf("%.*f", -13, 1e21)); + EXPECT_EQ("1000000000000000000000.000000", fmt::sprintf("%.*f", -13, 1e21)); } TEST(printf_test, make_printf_args) {