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, "") {