From fed26ae70a5701a85c088484fbcdd985622ad789 Mon Sep 17 00:00:00 2001 From: Yunare Maia Date: Fri, 4 Sep 2026 00:16:44 +0000 Subject: [PATCH] fix(error-code): sanitize non-UTF-8 bytes from ec.message() (fixes #4436) On Windows, std::error_code::message() may return a string in a system code page (e.g. CP-1252) containing bytes invalid in UTF-8. When formatted with {:s}, this caused encoding errors / crashes. Adds detail::append_utf8_sanitized() which validates UTF-8 input and replaces invalid byte sequences with U+FFFD (replacement character). The std::error_code formatter now uses this helper instead of appending ec.message() directly. --- include/fmt/std.h | 59 ++++++++++++++++++++++++++++++++++++++++++++++- test/std-test.cc | 17 ++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/include/fmt/std.h b/include/fmt/std.h index 28a7f6c9f60e..068857bb8bdb 100644 --- a/include/fmt/std.h +++ b/include/fmt/std.h @@ -290,6 +290,63 @@ struct has_format_as_member< T, void_t::format_as(std::declval()))>> : std::true_type {}; +// On Windows, std::error_code::message() may return a string in a system +// code page (e.g. CP-1252) that contains bytes invalid in UTF-8. This helper +// validates the input as UTF-8 and replaces any invalid byte sequences with +// U+FFFD (the Unicode replacement character), preventing encoding errors +// during formatting. See https://github.com/fmtlib/fmt/issues/4436. +inline void append_utf8_sanitized(basic_memory_buffer& buf, + const std::string& input) { + const unsigned char* p = + reinterpret_cast(input.data()); + const unsigned char* end = p + input.size(); + const char* repl = "\xEF\xBF\xBD"; + while (p < end) { + unsigned char b0 = *p; + // Decode UTF-8 sequence length from leading byte + int len; + if (b0 < 0x80) { + len = 1; + } else if ((b0 & 0xE0) == 0xC0) { + len = 2; + } else if ((b0 & 0xF0) == 0xE0) { + len = 3; + } else if ((b0 & 0xF8) == 0xF0) { + len = 4; + } else { + // Invalid leading byte — emit replacement and advance 1 + buf.append(repl, repl + 3); + ++p; + continue; + } + // Check we have enough continuation bytes + if (p + len > end) { + // Truncated sequence — emit replacement for each remaining byte + while (p < end) { + buf.append(repl, repl + 3); + ++p; + } + break; + } + // Validate continuation bytes (10xxxxxx) + bool valid = true; + for (int i = 1; i < len; ++i) { + if ((p[i] & 0xC0) != 0x80) { + valid = false; + break; + } + } + if (!valid) { + buf.append(repl, repl + 3); + ++p; + continue; + } + // Valid UTF-8 sequence — copy it + buf.append(reinterpret_cast(p), reinterpret_cast(p + len)); + p += len; + } +} + } // namespace detail template @@ -573,7 +630,7 @@ template <> struct formatter { ctx); auto buf = memory_buffer(); if (specs_.type() == presentation_type::string) { - buf.append(ec.message()); + detail::append_utf8_sanitized(buf, ec.message()); } else { buf.append(string_view(ec.category().name())); buf.push_back(':'); diff --git a/test/std-test.cc b/test/std-test.cc index 9cb02dcd7d5b..dc5b7b19569b 100644 --- a/test/std-test.cc +++ b/test/std-test.cc @@ -341,6 +341,23 @@ TEST(std_test, error_code) { "system:-42"); auto ec = std::make_error_code(std::errc::value_too_large); EXPECT_EQ(fmt::format("{:s}", ec), ec.message()); + // On Windows, ec.message() may contain bytes invalid in UTF-8 (system code + // page). The formatter must sanitize these to U+FFFD instead of crashing. + // Verify the sanitize helper directly with non-UTF-8 input. + { + std::string bad_input = "ok"; + bad_input.push_back(char(0xff)); + bad_input.push_back(char(0xfe)); + bad_input.append("bad"); + fmt::memory_buffer buf; + fmt::detail::append_utf8_sanitized(buf, bad_input); + std::string result(buf.data(), buf.size()); + std::string expected = "ok"; + expected.append({char(0xEF), char(0xBF), char(0xBD)}); + expected.append({char(0xEF), char(0xBF), char(0xBD)}); + expected.append("bad"); + EXPECT_EQ(result, expected); + } EXPECT_EQ(fmt::format("{:?}", std::error_code(42, generic)), "\"generic:42\""); EXPECT_EQ(fmt::format("{}",