PR #2093: reject unpaired surrogates when formatting wide strings Imported from GitHub PR https://github.com/abseil/abseil-cpp/pull/2093 **partial utf-8 emitted for unpaired surrogates in wide-string formatting** When a wide string passed to %ls (or a std::wstring) contains a high surrogate with no following low surrogate, ConvertStringArg keeps the two bytes WideToUtf8 writes for the first half of the pair and still reports success, so the formatted output carries a truncated utf-8 sequence. The single wchar_t path already rejects an unpaired surrogate via saw_high_surrogate, so this just brings the string path in line with it and adds a convert_test case for the trailing, mid-string and lone-low variants. Merge da58a96d88c5d6ddfeb014f14da47b8965de9cca into 0d1af721b72710afc6d50e880cc111eb852e4e22 Merging this change closes #2093 COPYBARA_INTEGRATE_REVIEW=https://github.com/abseil/abseil-cpp/pull/2093 from dxbjavid:wide-format-unpaired-surrogate da58a96d88c5d6ddfeb014f14da47b8965de9cca PiperOrigin-RevId: 945509522 Change-Id: Ifc1cdde9dc5c3850f5a01205c27ab97bda84e729
diff --git a/absl/strings/internal/str_format/arg.cc b/absl/strings/internal/str_format/arg.cc index fae98f4..0687b4e 100644 --- a/absl/strings/internal/str_format/arg.cc +++ b/absl/strings/internal/str_format/arg.cc
@@ -310,6 +310,8 @@ conv.has_left_flag()); } +inline bool IsLowSurrogate(uint32_t c) { return c >= 0xDC00 && c <= 0xDFFF; } + inline bool ConvertStringArg(const wchar_t *v, size_t len, const FormatConversionSpecImpl conv, @@ -325,11 +327,21 @@ strings_internal::ShiftState s; size_t chars_written = 0; for (size_t i = 0; i < len; ++i) { + // A high surrogate must be immediately followed by a low surrogate. If it + // isn't, the UTF-16 input is malformed and WideToUtf8() would otherwise + // leave a partial sequence in the buffer. The single wchar_t path already + // rejects an unpaired surrogate, so reject it here too. + if (s.saw_high_surrogate) { + const uint32_t cu = static_cast<uint32_t>(v[i]); + if (!IsLowSurrogate(cu)) return false; + } const size_t chars = strings_internal::WideToUtf8(v[i], &mb[chars_written], s); if (chars == static_cast<size_t>(-1)) { return false; } chars_written += chars; } + // A trailing high surrogate has no low surrogate to complete it. + if (s.saw_high_surrogate) return false; return ConvertStringArg(string_view(mb.data(), chars_written), conv, sink); }
diff --git a/absl/strings/internal/str_format/convert_test.cc b/absl/strings/internal/str_format/convert_test.cc index 1c3d1a3..5e86016 100644 --- a/absl/strings/internal/str_format/convert_test.cc +++ b/absl/strings/internal/str_format/convert_test.cc
@@ -357,6 +357,44 @@ EXPECT_EQ("ABC", FormatPack(wformat2, {FormatArgImpl(wp)})); } +TEST_F(FormatConvertTest, WideStringUnpairedSurrogate) { + // The single wchar_t ("%lc") path rejects an unpaired surrogate. The wide + // string ("%ls") path should reject it too rather than emitting a partial + // UTF-8 sequence. A failed conversion yields an empty result. + auto format_ls = [](const std::wstring& ws) { + UntypedFormatSpecImpl format("%ls"); + return FormatPack(format, {FormatArgImpl(ws)}); + }; + + // A well-formed surrogate pair (U+10000) still converts. + std::wstring pair; + pair.push_back(static_cast<wchar_t>(0xD800)); + pair.push_back(static_cast<wchar_t>(0xDC00)); + EXPECT_EQ("\xF0\x90\x80\x80", format_ls(pair)); + + // Trailing high surrogate with no low surrogate to complete it. + std::wstring trailing_high; + trailing_high.push_back(static_cast<wchar_t>(0xD800)); + EXPECT_EQ("", format_ls(trailing_high)); + + // High surrogate followed by a non-surrogate. + std::wstring high_then_ascii; + high_then_ascii.push_back(static_cast<wchar_t>(0xD800)); + high_then_ascii.push_back(L'A'); + EXPECT_EQ("", format_ls(high_then_ascii)); + + // High surrogate followed by another high surrogate. + std::wstring high_then_high; + high_then_high.push_back(static_cast<wchar_t>(0xD800)); + high_then_high.push_back(static_cast<wchar_t>(0xD800)); + EXPECT_EQ("", format_ls(high_then_high)); + + // Isolated low surrogate. + std::wstring lone_low; + lone_low.push_back(static_cast<wchar_t>(0xDC00)); + EXPECT_EQ("", format_ls(lone_low)); +} + // Pointer formatting is implementation defined. This checks that the argument // can be matched to `ptr`. MATCHER_P(MatchesPointerString, ptr, "") {