From 735ca7b166d1ea1e4fd01f3e64128b4e1524f96a Mon Sep 17 00:00:00 2001 From: sachhg Date: Thu, 10 Sep 2026 15:52:03 -0700 Subject: [PATCH 1/2] Fix printf conversion of a zero value with a precision of zero --- include/fmt/format.h | 5 +++++ include/fmt/printf.h | 10 +++++++++- test/printf-test.cc | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/include/fmt/format.h b/include/fmt/format.h index 318ce8144ab6..9e5124e6c7d6 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -2331,6 +2331,11 @@ FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, write_int_arg arg, return write_int_chr(out, abs_value, (prefix & 0xff) == '-', specs); } + // C requires no characters when converting a zero value with a precision of + // zero. Only printf can specify a precision for an integer; the format API + // rejects it. + if (specs.precision == 0 && abs_value == 0) begin = end; + // Write an integer in the format // // prefix contains chars in three lower bytes and the size in the fourth byte. diff --git a/include/fmt/printf.h b/include/fmt/printf.h index 8d792fbde8ee..77ffe030bb56 100644 --- a/include/fmt/printf.h +++ b/include/fmt/printf.h @@ -490,7 +490,10 @@ 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(); + // '#' has no effect on a zero value except for octal, where it forces a + // single '0'. The conversion specifier is not parsed yet, so remember it. + bool alt_zero = specs.alt() && arg.visit(is_zero_int()); + if (alt_zero) 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 +553,11 @@ 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. + 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 baf2b168e861..c7b3d750be29 100644 --- a/test/printf-test.cc +++ b/test/printf-test.cc @@ -278,6 +278,39 @@ 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("", "%#.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); From 1fcee224b6f70f1e5f128f83feef61bf6bf66d29 Mon Sep 17 00:00:00 2001 From: sachhg Date: Sun, 13 Sep 2026 11:02:25 -0700 Subject: [PATCH 2/2] Keep the zero precision handling in printf.h --- include/fmt/format.h | 5 ----- include/fmt/printf.h | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/include/fmt/format.h b/include/fmt/format.h index 9e5124e6c7d6..318ce8144ab6 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -2331,11 +2331,6 @@ FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, write_int_arg arg, return write_int_chr(out, abs_value, (prefix & 0xff) == '-', specs); } - // C requires no characters when converting a zero value with a precision of - // zero. Only printf can specify a precision for an integer; the format API - // rejects it. - if (specs.precision == 0 && abs_value == 0) begin = end; - // Write an integer in the format // // prefix contains chars in three lower bytes and the size in the fourth byte. diff --git a/include/fmt/printf.h b/include/fmt/printf.h index 77ffe030bb56..6a5424f0bbf7 100644 --- a/include/fmt/printf.h +++ b/include/fmt/printf.h @@ -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; }