diff --git a/absl/strings/BUILD.bazel b/absl/strings/BUILD.bazel index 4c974ce21a9..4f154e6cf5e 100644 --- a/absl/strings/BUILD.bazel +++ b/absl/strings/BUILD.bazel @@ -1258,6 +1258,7 @@ cc_test( ":pow10_helper", ":strings", "//absl/base:config", + "//absl/cleanup", "//absl/log", "//absl/numeric:int128", "//absl/random", diff --git a/absl/strings/CMakeLists.txt b/absl/strings/CMakeLists.txt index 3041e191be7..da689c142af 100644 --- a/absl/strings/CMakeLists.txt +++ b/absl/strings/CMakeLists.txt @@ -470,6 +470,7 @@ absl_cc_test( COPTS ${ABSL_TEST_COPTS} DEPS + absl::cleanup absl::config absl::core_headers absl::int128 diff --git a/absl/strings/numbers.cc b/absl/strings/numbers.cc index f0a8f002a18..8853d2eff1b 100644 --- a/absl/strings/numbers.cc +++ b/absl/strings/numbers.cc @@ -21,6 +21,7 @@ #include #include #include // for DBL_DIG and FLT_DIG +#include // for localeconv #include // for HUGE_VAL #include #include @@ -448,6 +449,30 @@ char* absl_nonnull numbers_internal::RoundTripDoubleToBuffer( ABSL_ASSERT(snprintf_result > 0 && snprintf_result < numbers_internal::kFastToBufferSize); } + + // snprintf() writes the radix character chosen by the global C locale's + // LC_NUMERIC category, so a process that has called setlocale() can end up + // with a separator other than '.' here. The rest of Abseil's float + // formatting (RoundTripFloatToBuffer, SixDigitsToBuffer) is locale- + // independent and SimpleAtod() only accepts '.', so rewrite the radix back + // to '.' to keep absl::HighPrecision(double) locale-independent and + // round-trippable through SimpleAtod(). + // TODO: Once all supported compilers ship std::to_chars with floating-point + // support, use it here for inherent locale independence. + const char* radix = localeconv()->decimal_point; + // Skip an empty decimal_point (some minimal environments leave it ""), which + // would otherwise match the NUL terminator and corrupt the buffer. + if (radix[0] != '\0' && std::strcmp(radix, ".") != 0) { + if (char* p = std::strstr(buffer, radix)) { + const size_t radix_len = std::strlen(radix); + *p = '.'; + // A multibyte radix (rare, but possible in some locales) leaves trailing + // bytes behind; collapse them so the output is a single '.'. + if (radix_len > 1) { + std::memmove(p + 1, p + radix_len, std::strlen(p + radix_len) + 1); + } + } + } return buffer; } diff --git a/absl/strings/numbers_test.cc b/absl/strings/numbers_test.cc index dd57a886b63..8ba114c9bf0 100644 --- a/absl/strings/numbers_test.cc +++ b/absl/strings/numbers_test.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/cleanup/cleanup.h" #include "absl/log/log.h" #include "absl/numeric/int128.h" #include "absl/random/random.h" @@ -1717,6 +1719,37 @@ class SimpleDtoaTest : public testing::Test { fenv_t fp_env_; }; +TEST(SimpleDtoa, HighPrecisionIsLocaleIndependent) { + // absl::HighPrecision(double) routes through RoundTripDoubleToBuffer(), which + // used to leak the global C locale's radix character (e.g. ',' under de_DE) + // into its output. HighPrecision() promises a value that SimpleAtod() reads + // back exactly, and SimpleAtod() only accepts '.', so the radix must stay '.' + // regardless of the active locale. + std::string old_locale = setlocale(LC_NUMERIC, nullptr); + auto restore_locale = absl::MakeCleanup( + [&] { setlocale(LC_NUMERIC, old_locale.c_str()); }); + const char* comma_locales[] = {"de_DE.UTF-8", "de_DE", "fr_FR.UTF-8", + "fr_FR", "nl_NL.UTF-8"}; + bool changed = false; + for (const char* loc : comma_locales) { + if (setlocale(LC_NUMERIC, loc) != nullptr) { + changed = true; + break; + } + } + if (!changed) { + GTEST_SKIP() << "No comma-radix locale available on this system."; + } + EXPECT_EQ(absl::StrCat(absl::HighPrecision(0.5)), "0.5"); + EXPECT_EQ(absl::StrCat(absl::HighPrecision(-1.25)), "-1.25"); + EXPECT_EQ(absl::StrCat(absl::HighPrecision(3.14159265358979)), + "3.14159265358979"); + double parsed = 0; + EXPECT_TRUE( + absl::SimpleAtod(absl::StrCat(absl::HighPrecision(0.1)), &parsed)); + EXPECT_EQ(parsed, 0.1); +} + // Run the given runnable functor for "cases" test cases, chosen over the // available range of float. pi and e and 1/e are seeded, and then all // available integer powers of 2 and 10 are multiplied against them. In