Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion include/fmt/std.h
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,63 @@ struct has_format_as_member<
T, void_t<decltype(formatter<T>::format_as(std::declval<const T&>()))>>
: 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<char>& buf,
const std::string& input) {
const unsigned char* p =
reinterpret_cast<const unsigned char*>(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<const char*>(p), reinterpret_cast<const char*>(p + len));
p += len;
}
}

} // namespace detail

template <typename T, typename Deleter>
Expand Down Expand Up @@ -573,7 +630,7 @@ template <> struct formatter<std::error_code> {
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(':');
Expand Down
17 changes: 17 additions & 0 deletions test/std-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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("{}",
Expand Down