diff --git a/Modules/Core/Common/CMakeLists.txt b/Modules/Core/Common/CMakeLists.txt index 2272c4f065c..0150efd59c4 100644 --- a/Modules/Core/Common/CMakeLists.txt +++ b/Modules/Core/Common/CMakeLists.txt @@ -158,7 +158,7 @@ try_compile( ${CMAKE_CURRENT_SOURCE_DIR}/CMake/itkCheckHasSchedGetAffinity.cxx ) -# Check for thread-local locale functions for NumericLocale +# Check for thread-safe locale functions (locale-independent parsing in itkStringConvert) try_compile( ITK_HAS_NEWLOCALE ${ITK_BINARY_DIR} diff --git a/Modules/Core/Common/include/itkNumericLocale.h b/Modules/Core/Common/include/itkNumericLocale.h deleted file mode 100644 index 767d88a8612..00000000000 --- a/Modules/Core/Common/include/itkNumericLocale.h +++ /dev/null @@ -1,84 +0,0 @@ -/*========================================================================= - * - * Copyright NumFOCUS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - *=========================================================================*/ -#ifndef itkNumericLocale_h -#define itkNumericLocale_h - -#include "itkMacro.h" -#include "ITKCommonExport.h" - -#include - -namespace itk -{ - -/** \class NumericLocale - * \brief RAII class for thread-safe temporary setting of LC_NUMERIC locale to "C". - * - * This class provides a thread-safe mechanism to temporarily set the LC_NUMERIC - * locale to "C" for locale-independent parsing and formatting of floating-point - * numbers. The original locale is automatically restored when the object goes - * out of scope. - * - * This is particularly useful when parsing file formats that use dot as decimal - * separator (like NRRD, VTK, etc.) regardless of the system locale setting. - * - * Thread safety: - * - On POSIX systems (when newlocale/uselocale are available): Uses thread-local locale - * - On Windows (when _configthreadlocale is available): Uses thread-specific locale - * - Fallback (when neither is available): Issues a warning if locale differs from "C", - * but does not change the locale. Applications must manage locale externally. - * - * Example usage: - * \code - * { - * NumericLocale cLocale; - * // Parse file with dot decimal separator - * double value = std::strtod("3.14159", nullptr); - * // Locale automatically restored here - * } - * \endcode - * - * \ingroup ITKCommon - */ -class ITKCommon_EXPORT NumericLocale -{ -public: - /** Constructor: Saves current LC_NUMERIC locale and sets it to "C" */ - NumericLocale(); - - /** Destructor: Restores the original LC_NUMERIC locale */ - ~NumericLocale(); - - // Delete copy and move operations - NumericLocale(const NumericLocale &) = delete; - NumericLocale & - operator=(const NumericLocale &) = delete; - NumericLocale(NumericLocale &&) = delete; - NumericLocale & - operator=(NumericLocale &&) = delete; - -private: - // Forward declaration of implementation structure - struct Impl; - // Pointer to implementation (pImpl idiom) - std::unique_ptr m_Impl; -}; - -} // end namespace itk - -#endif // itkNumericLocale_h diff --git a/Modules/Core/Common/include/itkStringConvert.h b/Modules/Core/Common/include/itkStringConvert.h index f1152eb4ec7..84188751a8b 100644 --- a/Modules/Core/Common/include/itkStringConvert.h +++ b/Modules/Core/Common/include/itkStringConvert.h @@ -43,6 +43,10 @@ namespace itk * `context` describing what was being parsed (e.g. * ``"NRRD header field 'sizes'"``) plus the offending input string. * + * The floating-point helpers parse with an explicit "C" locale, so `.` is the + * decimal separator regardless of the ambient `LC_NUMERIC` locale. Underflow to + * a subnormal or zero is a valid value; only overflow throws. + * * **Fixed-width integer return types** are used (`int32_t`, `int64_t`, * `uint32_t`, `uint64_t`) rather than the platform-dependent C++ * spellings (`int`, `long`, `unsigned long`). The C++ Standard only diff --git a/Modules/Core/Common/src/CMakeLists.txt b/Modules/Core/Common/src/CMakeLists.txt index d7da6aa50ba..174ab7cc1ac 100644 --- a/Modules/Core/Common/src/CMakeLists.txt +++ b/Modules/Core/Common/src/CMakeLists.txt @@ -106,7 +106,6 @@ set( itkMultipleLogOutput.cxx itkMultiThreaderBase.cxx itkNumberToString.cxx - itkNumericLocale.cxx itkNumericTraits.cxx itkNumericTraitsCovariantVectorPixel.cxx itkNumericTraitsDiffusionTensor3DPixel.cxx diff --git a/Modules/Core/Common/src/itkNumericLocale.cxx b/Modules/Core/Common/src/itkNumericLocale.cxx deleted file mode 100644 index 600615e3e4b..00000000000 --- a/Modules/Core/Common/src/itkNumericLocale.cxx +++ /dev/null @@ -1,156 +0,0 @@ -/*========================================================================= - * - * Copyright NumFOCUS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - *=========================================================================*/ - -#include "itkNumericLocale.h" -#include "itkConfigurePrivate.h" -#include "itkObject.h" - -#include -#include -#include - -// Include platform-specific headers based on detected features -#ifdef ITK_HAS_NEWLOCALE -# if defined(__APPLE__) -# include -# endif -#endif - -namespace itk -{ - -// Implementation structure definition -struct NumericLocale::Impl -{ -#ifdef ITK_HAS_CONFIGTHREADLOCALE - // Windows: thread-specific locale - int m_PreviousThreadLocaleSetting{ -1 }; - char * m_SavedLocale{ nullptr }; -#elif defined(ITK_HAS_NEWLOCALE) - // POSIX: thread-local locale - locale_t m_PreviousLocale{ nullptr }; - locale_t m_CLocale{ nullptr }; -#else - // Fallback: no locale change, only check and warn - bool m_WarningIssued{ false }; -#endif -}; - -#ifdef ITK_HAS_CONFIGTHREADLOCALE - -// Windows implementation using thread-specific locale -NumericLocale::NumericLocale() - : m_Impl(new Impl()) -{ - // Enable thread-specific locale for this thread - m_Impl->m_PreviousThreadLocaleSetting = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); - - // Save current LC_NUMERIC locale - const char * currentLocale = setlocale(LC_NUMERIC, nullptr); - if (currentLocale) - { - m_Impl->m_SavedLocale = _strdup(currentLocale); - // If _strdup fails (returns nullptr), m_SavedLocale remains nullptr - // and the locale will not be restored in the destructor - } - - // Set to C locale for parsing - setlocale(LC_NUMERIC, "C"); -} - -NumericLocale::~NumericLocale() -{ - // Restore original locale - if (m_Impl->m_SavedLocale) - { - setlocale(LC_NUMERIC, m_Impl->m_SavedLocale); - free(m_Impl->m_SavedLocale); - } - - // Restore previous thread-specific locale setting - if (m_Impl->m_PreviousThreadLocaleSetting != -1) - { - _configthreadlocale(m_Impl->m_PreviousThreadLocaleSetting); - } -} - -#elif defined(ITK_HAS_NEWLOCALE) - -// POSIX implementation using thread-local locale (uselocale/newlocale) -NumericLocale::NumericLocale() - : m_Impl(new Impl()) -{ - // Create a new C locale - // If newlocale fails (returns nullptr), m_CLocale remains nullptr - // and the locale will not be changed - this is a safe fallback - m_Impl->m_CLocale = newlocale(LC_NUMERIC_MASK, "C", nullptr); - - if (m_Impl->m_CLocale) - { - // Set the C locale for this thread and save the previous locale - // uselocale returns the previous locale, which may be LC_GLOBAL_LOCALE - m_Impl->m_PreviousLocale = uselocale(m_Impl->m_CLocale); - } -} - -NumericLocale::~NumericLocale() -{ - // If we created and installed a C locale for this thread, - // restore the previous locale and free the C locale. - // Always restore if m_CLocale is set, as m_PreviousLocale may be - // LC_GLOBAL_LOCALE which is a valid non-null value. - if (m_Impl->m_CLocale) - { - uselocale(m_Impl->m_PreviousLocale); - freelocale(m_Impl->m_CLocale); - } -} - -#else - -// Fallback implementation - only check locale and issue warning if not "C" -// Do not modify the locale, application must manage it externally -NumericLocale::NumericLocale() - : m_Impl(new Impl()) -{ - // Check if current locale is compatible with expected "C" locale - const char * currentLocale = setlocale(LC_NUMERIC, nullptr); - if (currentLocale && std::strcmp(currentLocale, "C") != 0) - { - // Issue warning only once per instance - if (::itk::Object::GetGlobalWarningDisplay()) - { - std::ostringstream itkmsg; - itkmsg << "LC_NUMERIC locale is '" << currentLocale << "' (not 'C'). " - << "Thread-safe locale functions not available. " - << "Locale-dependent number parsing may cause issues. " - << "Please manage locale at application level."; - ::itk::OutputWindowDisplayWarningText(__FILE__, __LINE__, "NumericLocale", nullptr, itkmsg.str().c_str()); - } - m_Impl->m_WarningIssued = true; - } -} - -NumericLocale::~NumericLocale() -{ - // No action needed in fallback - we don't change the locale -} - -#endif - -} // end namespace itk diff --git a/Modules/Core/Common/src/itkStringConvert.cxx b/Modules/Core/Common/src/itkStringConvert.cxx index 5c427ec5cfb..bf62dbf0733 100644 --- a/Modules/Core/Common/src/itkStringConvert.cxx +++ b/Modules/Core/Common/src/itkStringConvert.cxx @@ -16,11 +16,26 @@ * *=========================================================================*/ #include "itkStringConvert.h" +#include "itkConfigurePrivate.h" +#include +#include #include +#include #include #include #include +#include + +// A locale-aware strtod would silently parse "0.5" as 0 under a decimal-comma LC_NUMERIC. +#if !defined(ITK_HAS_NEWLOCALE) && !defined(ITK_HAS_CONFIGTHREADLOCALE) +# error "ITK requires newlocale() or _configthreadlocale() for locale-independent numeric parsing" +#endif + +#include +#if defined(ITK_HAS_NEWLOCALE) && defined(__APPLE__) +# include +#endif namespace itk { @@ -71,6 +86,69 @@ RejectLeadingMinus(const char * context, const std::string & str, const char * t } } } + +// Parse against an explicit "C" locale so '.' is always the decimal separator. +#if defined(ITK_HAS_NEWLOCALE) +locale_t +GetCLocale() +{ + static const locale_t cLocale = newlocale(LC_NUMERIC_MASK, "C", static_cast(nullptr)); + if (!cLocale) + { + throw std::runtime_error("cannot create \"C\" numeric locale"); + } + return cLocale; +} +#else +_locale_t +GetCLocale() +{ + static const _locale_t cLocale = _create_locale(LC_NUMERIC, "C"); + if (!cLocale) + { + throw std::runtime_error("cannot create \"C\" numeric locale"); + } + return cLocale; +} +#endif + +// ERANGE with an infinite result is overflow; ERANGE underflow yields a valid subnormal or zero. +template +TValue +ParseFloatingPointCLocale(const std::string & str) +{ + static_assert(std::is_same_v || std::is_same_v, + "ParseFloatingPointCLocale supports only double and float"); + const char * begin = str.c_str(); + char * end = nullptr; + errno = 0; + TValue value{}; + if constexpr (std::is_same_v) + { +#if defined(ITK_HAS_NEWLOCALE) + value = strtof_l(begin, &end, GetCLocale()); +#else + value = _strtof_l(begin, &end, GetCLocale()); +#endif + } + else + { +#if defined(ITK_HAS_NEWLOCALE) + value = strtod_l(begin, &end, GetCLocale()); +#else + value = _strtod_l(begin, &end, GetCLocale()); +#endif + } + if (end == begin) + { + throw std::invalid_argument("no conversion"); + } + if (errno == ERANGE && std::isinf(value)) + { + throw std::out_of_range("out of range"); + } + return value; +} } // namespace @@ -166,7 +244,7 @@ StringToDouble(const std::string & str, const char * context) { try { - return std::stod(str); + return ParseFloatingPointCLocale(str); } catch (const std::invalid_argument & e) { @@ -176,6 +254,10 @@ StringToDouble(const std::string & str, const char * context) { ThrowParseFailure(context, str, "double", e.what(), "out_of_range"); } + catch (const std::runtime_error & e) + { + ThrowParseFailure(context, str, "double", e.what(), "runtime_error"); + } } @@ -184,7 +266,7 @@ StringToFloat(const std::string & str, const char * context) { try { - return std::stof(str); + return ParseFloatingPointCLocale(str); } catch (const std::invalid_argument & e) { @@ -194,6 +276,10 @@ StringToFloat(const std::string & str, const char * context) { ThrowParseFailure(context, str, "float", e.what(), "out_of_range"); } + catch (const std::runtime_error & e) + { + ThrowParseFailure(context, str, "float", e.what(), "runtime_error"); + } } } // namespace itk diff --git a/Modules/Core/Common/test/CMakeLists.txt b/Modules/Core/Common/test/CMakeLists.txt index b4eff31e38e..18e63994a63 100644 --- a/Modules/Core/Common/test/CMakeLists.txt +++ b/Modules/Core/Common/test/CMakeLists.txt @@ -1499,7 +1499,6 @@ set( itkNeighborhoodAllocatorGTest.cxx itkNeighborhoodIteratorsGTest.cxx itkNumberToStringGTest.cxx - itkNumericLocaleGTest.cxx itkNumericsGTest.cxx itkObjectFactoryBaseGTest.cxx itkOffsetGTest.cxx diff --git a/Modules/Core/Common/test/itkNumericLocaleGTest.cxx b/Modules/Core/Common/test/itkNumericLocaleGTest.cxx deleted file mode 100644 index bcec7286b62..00000000000 --- a/Modules/Core/Common/test/itkNumericLocaleGTest.cxx +++ /dev/null @@ -1,169 +0,0 @@ -/*========================================================================= - * - * Copyright NumFOCUS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - *=========================================================================*/ - -#include "itkNumericLocale.h" -#include -#include -#include -#include - -// Test that NumericLocale temporarily sets LC_NUMERIC to "C" -TEST(NumericLocale, TemporarilySetsToCLocale) -{ - // Save initial locale - const char * initialLocale = setlocale(LC_NUMERIC, nullptr); - ASSERT_NE(initialLocale, nullptr); - std::string savedInitialLocale(initialLocale); - - { - // Create NumericLocale - should set to "C" - itk::NumericLocale numericLocale; - - const char * currentLocale = setlocale(LC_NUMERIC, nullptr); - ASSERT_NE(currentLocale, nullptr); - EXPECT_STREQ(currentLocale, "C"); - } - - // After NumericLocale destroyed, locale should be restored - const char * restoredLocale = setlocale(LC_NUMERIC, nullptr); - ASSERT_NE(restoredLocale, nullptr); - EXPECT_STREQ(restoredLocale, savedInitialLocale.c_str()); -} - -// Test that numeric parsing works correctly with NumericLocale -TEST(NumericLocale, ParsesFloatsWithDotDecimalSeparator) -{ - { - itk::NumericLocale numericLocale; - - // Parse floating-point number with dot as decimal separator - double value = std::strtod("3.14159", nullptr); - EXPECT_DOUBLE_EQ(value, 3.14159); - - value = std::strtod("0.878906", nullptr); - EXPECT_DOUBLE_EQ(value, 0.878906); - - value = std::strtod("2.5", nullptr); - EXPECT_DOUBLE_EQ(value, 2.5); - } -} - -// Test that NumericLocale can be nested -TEST(NumericLocale, SupportsNesting) -{ - const char * initialLocale = setlocale(LC_NUMERIC, nullptr); - ASSERT_NE(initialLocale, nullptr); - std::string savedInitialLocale(initialLocale); - - { - itk::NumericLocale outerLocale; - EXPECT_STREQ(setlocale(LC_NUMERIC, nullptr), "C"); - - { - itk::NumericLocale innerLocale; - EXPECT_STREQ(setlocale(LC_NUMERIC, nullptr), "C"); - } - - // After inner destroyed, still in C locale - EXPECT_STREQ(setlocale(LC_NUMERIC, nullptr), "C"); - } - - // After both destroyed, locale is restored - EXPECT_STREQ(setlocale(LC_NUMERIC, nullptr), savedInitialLocale.c_str()); -} - -// Test with a different locale if available (optional test) -TEST(NumericLocale, WorksWithDifferentInitialLocale) -{ - // Try to set a locale with comma as decimal separator - const char * decimalCommaLocales[] = { "de_DE.UTF-8", "bs_BA.UTF-8", "bs-Latn-BA.UTF-8", "fr_FR.UTF-8" }; - - unsigned countAvailableLocales = 0; - for (const char * locale : decimalCommaLocales) - { - const char * decimalCommaLocale = setlocale(LC_NUMERIC, locale); - if (decimalCommaLocale != nullptr) - { - // Verify we're in German locale (comma separator) - const char * currentLocale = setlocale(LC_NUMERIC, nullptr); - ASSERT_NE(currentLocale, nullptr); - std::string savedLocale(currentLocale); - - // Without NumericLocale, parsing with dot would fail in German locale - // (This test verifies the problem we're fixing) - double valueWithoutFix = std::strtod("0.878906", nullptr); - // In de_DE locale, this would parse as 0.0 (stops at dot) - EXPECT_EQ(valueWithoutFix, 0.0); - - { - // With NumericLocale, parsing should work correctly - itk::NumericLocale numericLocale; - - double valueWithFix = std::strtod("0.878906", nullptr); - EXPECT_DOUBLE_EQ(valueWithFix, 0.878906); - - double value2 = std::strtod("3.5", nullptr); - EXPECT_DOUBLE_EQ(value2, 3.5); - } - - // After NumericLocale destroyed, we should be back in German locale - EXPECT_STREQ(setlocale(LC_NUMERIC, nullptr), savedLocale.c_str()); - - // Restore to C locale for other tests - setlocale(LC_NUMERIC, "C"); - ++countAvailableLocales; - } - } - if (countAvailableLocales == 0) - { - GTEST_SKIP() << "Decimal-comma locales not available on this system. Locales tried: " - << sizeof(decimalCommaLocales) / sizeof(char *) - << ". Consider installing locales by\nsudo locale-gen de_DE.UTF-8"; - } - else - { - std::cout << "Passed. Tested with " << countAvailableLocales << " decimal-comma locales." << std::endl; - } -} - -// Test that multiple sequential uses work correctly -TEST(NumericLocale, SupportsSequentialUses) -{ - for (int i = 0; i < 3; ++i) - { - itk::NumericLocale numericLocale; - double value = std::strtod("1.5", nullptr); - EXPECT_DOUBLE_EQ(value, 1.5); - } -} - -// Test basic RAII behavior -TEST(NumericLocale, BasicRAII) -{ - const char * initialLocale = setlocale(LC_NUMERIC, nullptr); - ASSERT_NE(initialLocale, nullptr); - std::string savedInitialLocale(initialLocale); - - // Create and immediately destroy - { - itk::NumericLocale temp; - } - - // Locale should be restored - EXPECT_STREQ(setlocale(LC_NUMERIC, nullptr), savedInitialLocale.c_str()); -} diff --git a/Modules/Core/Common/test/itkStringConvertGTest.cxx b/Modules/Core/Common/test/itkStringConvertGTest.cxx index 90cd245168b..123b9516a72 100644 --- a/Modules/Core/Common/test/itkStringConvertGTest.cxx +++ b/Modules/Core/Common/test/itkStringConvertGTest.cxx @@ -19,7 +19,9 @@ #include "itkGTest.h" #include "itkStringConvert.h" #include "itkMacro.h" +#include "itkTestNumericLocale.h" +#include #include #include #include @@ -237,6 +239,48 @@ TEST(StringTools, StringToDouble_Bad_Throws) EXPECT_THROW(itk::StringToDouble("abc", "ctx"), itk::ExceptionObject); } +TEST(StringTools, StringToDouble_OverflowThrows_UnderflowAccepted) +{ + EXPECT_THROW(itk::StringToDouble("1e9999", "ctx"), itk::ExceptionObject); + EXPECT_THROW(itk::StringToDouble("-1e9999", "ctx"), itk::ExceptionObject); + // ERANGE underflow yields a representable subnormal or zero: accepted. + EXPECT_NEAR(itk::StringToDouble("1e-320", "ctx"), 1e-320, 1e-321); + EXPECT_DOUBLE_EQ(itk::StringToDouble("1e-400", "ctx"), 0.0); +} + +// Parsing must honor '.' as decimal separator under decimal-comma locales, +// where a locale-aware parse would truncate "0.878906" to 0 (issue #5683). +TEST(StringTools, StringToDouble_LocaleIndependent) +{ + unsigned int tested = 0; + for (const char * const commaLocale : itk::test::commaDecimalLocales) + { + if (setlocale(LC_NUMERIC, commaLocale) == nullptr) + { + continue; + } + ++tested; + EXPECT_NEAR(itk::StringToDouble("0.878906", "ctx"), 0.878906, 1e-9); + EXPECT_NEAR(itk::StringToFloat("3.5", "ctx"), 3.5f, 1e-6); + // The overflow/underflow contract must hold under a comma locale too. + EXPECT_THROW(itk::StringToDouble("1e9999", "ctx"), itk::ExceptionObject); + EXPECT_THROW(itk::StringToFloat("1e9999", "ctx"), itk::ExceptionObject); + EXPECT_DOUBLE_EQ(itk::StringToDouble("1e-400", "ctx"), 0.0); + // A comma is a separator, not a decimal point: parse stops before it. + EXPECT_DOUBLE_EQ(itk::StringToDouble("0,878906", "ctx"), 0.0); + } + setlocale(LC_NUMERIC, "C"); + EXPECT_DOUBLE_EQ(itk::StringToDouble("2.25", "ctx"), 2.25); + if (tested == 0) + { + if (itk::test::CommaDecimalLocaleIsExpected()) + { + FAIL() << itk::test::NoCommaDecimalLocaleMessage(); + } + GTEST_SKIP() << itk::test::NoCommaDecimalLocaleMessage(); + } +} + // ----- StringToFloat ----- diff --git a/Modules/Core/TestKernel/include/itkTestNumericLocale.h b/Modules/Core/TestKernel/include/itkTestNumericLocale.h new file mode 100644 index 00000000000..cf5d428f81b --- /dev/null +++ b/Modules/Core/TestKernel/include/itkTestNumericLocale.h @@ -0,0 +1,53 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#ifndef itkTestNumericLocale_h +#define itkTestNumericLocale_h + +#include + +namespace itk::test +{ + +/** Decimal-comma locale names in POSIX and Windows spelling; a test loops over + * these and skips the names its runner does not provide. */ +inline constexpr const char * const commaDecimalLocales[] = { "de_DE.UTF-8", "fr_FR.UTF-8", "nl_NL.UTF-8", + "it_IT.UTF-8", "de-DE", "German_Germany.1252", + "French_France.1252" }; + +/** True where the OS ships decimal-comma locales unconditionally, so finding + * none is a broken runner; a Linux image legitimately may have no `locale-gen`. */ +inline bool +CommaDecimalLocaleIsExpected() +{ +#if defined(__APPLE__) || defined(_WIN32) + return true; +#else + return false; +#endif +} + +inline std::string +NoCommaDecimalLocaleMessage() +{ + return "No decimal-comma locale installed, so the locale-independent parse and format paths were " + "NOT exercised on this runner. Install e.g. de_DE.UTF-8 (Linux: locale-gen) to cover them."; +} + +} // namespace itk::test + +#endif // itkTestNumericLocale_h diff --git a/Modules/IO/IOTransformDCMTK/src/itkDCMTKTransformIO.hxx b/Modules/IO/IOTransformDCMTK/src/itkDCMTKTransformIO.hxx index 4134db729ed..18f4edfd784 100644 --- a/Modules/IO/IOTransformDCMTK/src/itkDCMTKTransformIO.hxx +++ b/Modules/IO/IOTransformDCMTK/src/itkDCMTKTransformIO.hxx @@ -22,8 +22,8 @@ #include "itkAffineTransform.h" #include "itkCompositeTransform.h" #include "itkEuler3DTransform.h" -#include "itkNumericLocale.h" #include "itkSimilarity3DTransform.h" +#include "itkStringConvert.h" #include #include @@ -90,9 +90,7 @@ template void DCMTKTransformIO::Read() { - // DICOM Decimal-String values use '.' as decimal separator regardless - // of system locale; force LC_NUMERIC=C for std::stod parsing below. - const NumericLocale cLocale; + constexpr auto matrixEntryContext = "DICOM FrameOfReferenceTransformationMatrix entry"; TransformListType & transformList = this->GetReadTransformList(); transformList.clear(); @@ -215,7 +213,7 @@ DCMTKTransformIO::Read() itkExceptionMacro("Could not get expected matrix entry."); } ++matrixEntryIndex; - transformParameters[row * Dimension + col] = std::stod(matrixString.c_str()); + transformParameters[row * Dimension + col] = StringToDouble(matrixString.c_str(), matrixEntryContext); } result = currentMatrixSequenceItem->findAndGetOFString( DCM_FrameOfReferenceTransformationMatrix, matrixString, matrixEntryIndex); @@ -224,7 +222,8 @@ DCMTKTransformIO::Read() itkExceptionMacro("Could not get expected matrix entry."); } ++matrixEntryIndex; - transformParameters[Dimension * Dimension + row] = std::stod(matrixString.c_str()); + transformParameters[Dimension * Dimension + row] = + StringToDouble(matrixString.c_str(), matrixEntryContext); } affineTransform->SetParameters(transformParameters); diff --git a/Modules/IO/NRRD/src/itkNrrdImageIO.cxx b/Modules/IO/NRRD/src/itkNrrdImageIO.cxx index 3b6c17b9649..e080a435dae 100644 --- a/Modules/IO/NRRD/src/itkNrrdImageIO.cxx +++ b/Modules/IO/NRRD/src/itkNrrdImageIO.cxx @@ -22,7 +22,6 @@ #include "itkMetaDataObject.h" #include "itkIOCommon.h" #include "itkFloatingPointExceptions.h" -#include "itkNumericLocale.h" #include "itkNumberToString.h" #include @@ -599,12 +598,6 @@ NrrdImageIO::ReadImageInformation() FloatingPointExceptions::Disable(); } - // Set LC_NUMERIC to "C" locale to ensure locale-independent parsing - // of floating-point values in NRRD headers (spacing, origin, direction, etc.). - // This prevents issues in locales that use comma as decimal separator. - // Using thread-safe NumericLocale from ITKCommon. - NumericLocale cLocale; - // this is the mechanism by which we tell nrrdLoad to read // just the header, and none of the data nrrdIoStateSet(nio, nrrdIoStateSkipData, 1); @@ -1138,11 +1131,6 @@ NrrdImageIO::Read(void * buffer) FloatingPointExceptions::Disable(); #endif - // Set LC_NUMERIC to "C" locale to ensure locale-independent parsing - // of floating-point values in NRRD headers. - // Using thread-safe NumericLocale from ITKCommon. - NumericLocale cLocale; - // Read in the nrrd. Yes, this means that the header is being read // twice: once by NrrdImageIO::ReadImageInformation, and once here if (nrrdLoad(nrrd, this->GetFileName(), nullptr) != 0) @@ -1460,11 +1448,6 @@ NrrdImageIO::Write(const void * buffer) break; } - // Set LC_NUMERIC to "C" locale to ensure locale-independent formatting - // of floating-point values when writing NRRD headers. - // Using thread-safe NumericLocale from ITKCommon. - NumericLocale cLocale; - // Write the nrrd to file. if (nrrdSave(this->GetFileName(), nrrd, nio)) { diff --git a/Modules/IO/NRRD/test/itkNrrdLocaleTest.cxx b/Modules/IO/NRRD/test/itkNrrdLocaleTest.cxx index a52b99b6759..edca3156c20 100644 --- a/Modules/IO/NRRD/test/itkNrrdLocaleTest.cxx +++ b/Modules/IO/NRRD/test/itkNrrdLocaleTest.cxx @@ -22,6 +22,7 @@ #include "itkImageFileWriter.h" #include "itkNrrdImageIO.h" #include "itkTestingMacros.h" +#include "itkTestNumericLocale.h" // Test that NRRD reader handles locale-dependent parsing correctly. // This test verifies the fix for: https://github.com/InsightSoftwareConsortium/ITK/issues/5683 @@ -123,11 +124,16 @@ itkNrrdLocaleTest(int argc, char * argv[]) } } - // Test 2: Read with de_DE locale (the problematic case) - // Try to set German locale; if not available, skip this part - if (setlocale(LC_NUMERIC, "de_DE.UTF-8") != nullptr) + // Test 2: read under a decimal-comma locale, the only path exercising the parse. + unsigned int localesTested = 0; + for (const char * const commaLocale : itk::test::commaDecimalLocales) { - std::cout << "Testing with de_DE.UTF-8 locale..." << std::endl; + if (setlocale(LC_NUMERIC, commaLocale) == nullptr) + { + continue; + } + ++localesTested; + std::cout << "Testing with " << commaLocale << " locale..." << std::endl; using ReaderType = itk::ImageFileReader; auto reader = ReaderType::New(); @@ -140,15 +146,15 @@ itkNrrdLocaleTest(int argc, char * argv[]) ImageType::SpacingType readSpacing = readImage->GetSpacing(); ImageType::PointType readOrigin = readImage->GetOrigin(); - std::cout << "de_DE locale - Read spacing: " << readSpacing << std::endl; - std::cout << "de_DE locale - Read origin: " << readOrigin << std::endl; + std::cout << commaLocale << " - Read spacing: " << readSpacing << std::endl; + std::cout << commaLocale << " - Read origin: " << readOrigin << std::endl; // Verify spacing - this is the critical test for (unsigned int i = 0; i < Dimension; ++i) { if (itk::Math::Absolute(readSpacing[i] - spacing[i]) > 1e-6) { - std::cerr << "Spacing mismatch in de_DE locale at index " << i << std::endl; + std::cerr << "Spacing mismatch in " << commaLocale << " locale at index " << i << std::endl; std::cerr << "Expected: " << spacing[i] << ", Got: " << readSpacing[i] << std::endl; std::cerr << "This indicates locale-dependent parsing is still occurring!" << std::endl; return EXIT_FAILURE; @@ -160,19 +166,60 @@ itkNrrdLocaleTest(int argc, char * argv[]) { if (itk::Math::Absolute(readOrigin[i] - origin[i]) > 1e-6) { - std::cerr << "Origin mismatch in de_DE locale at index " << i << std::endl; + std::cerr << "Origin mismatch in " << commaLocale << " locale at index " << i << std::endl; std::cerr << "Expected: " << origin[i] << ", Got: " << readOrigin[i] << std::endl; std::cerr << "This indicates locale-dependent parsing is still occurring!" << std::endl; return EXIT_FAILURE; } } + } + setlocale(LC_NUMERIC, "C"); - // Restore C locale + // Test 3: WRITE under a decimal-comma locale, read back under "C". A + // locale-dependent writer would emit "0,878906" and corrupt the file. + for (const char * const commaLocale : itk::test::commaDecimalLocales) + { + if (setlocale(LC_NUMERIC, commaLocale) == nullptr) + { + continue; + } + const std::string commaFilename = std::string(argv[1]) + "/locale_test_written_under_comma.nrrd"; + auto commaWriter = WriterType::New(); + commaWriter->SetFileName(commaFilename); + commaWriter->SetInput(image); + commaWriter->SetImageIO(itk::NrrdImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(commaWriter->Update()); setlocale(LC_NUMERIC, "C"); + + using ReaderType = itk::ImageFileReader; + auto reader = ReaderType::New(); + reader->SetFileName(commaFilename); + reader->SetImageIO(itk::NrrdImageIO::New()); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + + const ImageType::SpacingType readSpacing = reader->GetOutput()->GetSpacing(); + for (unsigned int i = 0; i < Dimension; ++i) + { + if (itk::Math::Absolute(readSpacing[i] - spacing[i]) > 1e-6) + { + std::cerr << "Spacing mismatch after writing under " << commaLocale << " at index " << i << std::endl; + std::cerr << "Expected: " << spacing[i] << ", Got: " << readSpacing[i] << std::endl; + std::cerr << "This indicates locale-dependent FORMATTING during write!" << std::endl; + return EXIT_FAILURE; + } + } + break; // one comma locale suffices for the write path } - else + setlocale(LC_NUMERIC, "C"); + + if (localesTested == 0) { - std::cout << "de_DE.UTF-8 locale not available, skipping locale-specific test" << std::endl; + if (itk::test::CommaDecimalLocaleIsExpected()) + { + std::cerr << itk::test::NoCommaDecimalLocaleMessage() << std::endl; + return EXIT_FAILURE; + } + std::cout << "WARNING: " << itk::test::NoCommaDecimalLocaleMessage() << std::endl; } std::cout << "Test finished successfully." << std::endl; diff --git a/Modules/ThirdParty/NrrdIO/src/CMakeLists.txt b/Modules/ThirdParty/NrrdIO/src/CMakeLists.txt index e86d7a69951..24a04eae2a4 100644 --- a/Modules/ThirdParty/NrrdIO/src/CMakeLists.txt +++ b/Modules/ThirdParty/NrrdIO/src/CMakeLists.txt @@ -14,6 +14,10 @@ set(ZLIB_LIBRARY ITK::ITKZLIBModule) list(APPEND NRRDIO_COMPRESSION_LIBRARIES ITK::ITKZLIBModule) if(UNIX) list(APPEND NRRDIO_COMPRESSION_LIBRARIES m) + # parseAir.c uses pthread_once for its cached "C" locale; on glibc < 2.34 + # pthread_once lives in libpthread, not libc. + find_package(Threads REQUIRED) + list(APPEND NRRDIO_COMPRESSION_LIBRARIES Threads::Threads) list(REMOVE_DUPLICATES NRRDIO_COMPRESSION_LIBRARIES) endif() diff --git a/Modules/ThirdParty/NrrdIO/src/NrrdIO/itk_NrrdIO_mangle.h.in b/Modules/ThirdParty/NrrdIO/src/NrrdIO/itk_NrrdIO_mangle.h.in index 63cfe248928..3a29247b28c 100644 --- a/Modules/ThirdParty/NrrdIO/src/NrrdIO/itk_NrrdIO_mangle.h.in +++ b/Modules/ThirdParty/NrrdIO/src/NrrdIO/itk_NrrdIO_mangle.h.in @@ -113,6 +113,7 @@ number, from inclusion. #define airTeemReleaseDone @MANGLE_PREFIX@_airTeemReleaseDone #define airTeemVersion @MANGLE_PREFIX@_airTeemVersion #define airTeemVersionSprint @MANGLE_PREFIX@_airTeemVersionSprint +#define air__CLocale @MANGLE_PREFIX@_air__CLocale #define air__SanityHelper @MANGLE_PREFIX@_air__SanityHelper #define biffAdd @MANGLE_PREFIX@_biffAdd #define biffAddf @MANGLE_PREFIX@_biffAddf diff --git a/Modules/ThirdParty/NrrdIO/src/NrrdIO/miscAir.c b/Modules/ThirdParty/NrrdIO/src/NrrdIO/miscAir.c index bdf4735b8a5..b79a0b893ca 100644 --- a/Modules/ThirdParty/NrrdIO/src/NrrdIO/miscAir.c +++ b/Modules/ThirdParty/NrrdIO/src/NrrdIO/miscAir.c @@ -23,6 +23,25 @@ 3. This notice may not be removed or altered from any source distribution. */ +/* Locale-independent numeric printing for airSinglePrintf; the API selection + must precede the first system-header include (NrrdIO.h pulls in ). + BSD/macOS and Windows have printf-family *_l variants; glibc/musl lack + them, but their printf honors the uselocale() thread-local locale. */ +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) \ + || defined(__OpenBSD__) || defined(__DragonFly__) +# include +# define TEEM_HAS_PRINTF_L 1 +#elif defined(__linux__) +# ifndef _GNU_SOURCE +# define _GNU_SOURCE 1 +# endif +# include +# define TEEM_HAS_USELOCALE 1 +#elif defined(_MSC_VER) +# include +# define TEEM_HAS_PRINTF_L_WIN 1 +#endif + #include "NrrdIO.h" #include "privateAir.h" /* timer functions */ @@ -36,6 +55,37 @@ # include #endif +#if defined(TEEM_HAS_PRINTF_L_WIN) +/* _snprintf_l leaves the buffer unterminated and returns -1 when the output + does not fit; these restore the C99 snprintf contract that the rest of + nrrdSprint[] follows (always terminate, return the would-be length). */ +static int +_airVsnprintfCLoc(char *str, size_t strSize, const char *fmt, _locale_t cloc, va_list ap) { + va_list ap2; + int ret; + va_copy(ap2, ap); + ret = _vsnprintf_l(str, strSize, fmt, cloc, ap); + if (ret < 0) { + if (strSize) { + str[strSize - 1] = '\0'; + } + ret = _vscprintf_l(fmt, cloc, ap2); + } + va_end(ap2); + return ret; +} + +static int +_airSnprintfCLoc(char *str, size_t strSize, const char *fmt, _locale_t cloc, ...) { + va_list ap; + int ret; + va_start(ap, cloc); + ret = _airVsnprintfCLoc(str, strSize, fmt, cloc, ap); + va_end(ap); + return ret; +} +#endif + /* ******** airTeemVersion ******** airTeemReleaseDone @@ -211,8 +261,27 @@ airSinglePrintf(FILE *file, char *str, size_t strSize, const char *_fmt, ...) { int ret, isF, isD; const char *_p0, *_p1, *_p2, *_p3, *_p4, *_p5; va_list ap; +#if defined(TEEM_HAS_PRINTF_L) || defined(TEEM_HAS_USELOCALE) + locale_t _aspCloc = (locale_t)air__CLocale(); +#elif defined(TEEM_HAS_PRINTF_L_WIN) + _locale_t _aspCloc = (_locale_t)air__CLocale(); +#endif +#if defined(TEEM_HAS_USELOCALE) + /* swap this thread to the "C" numeric locale for the whole function; + restored at the single exit below */ + locale_t _aspOld = _aspCloc ? uselocale(_aspCloc) : (locale_t)0; +#endif va_start(ap, _fmt); +#if defined(TEEM_HAS_PRINTF_L) || defined(TEEM_HAS_USELOCALE) \ + || defined(TEEM_HAS_PRINTF_L_WIN) + /* cannot print locale-independently without the cached "C" locale: fail + (printf error convention) rather than risk a wrong decimal separator */ + if (!_aspCloc) { + va_end(ap); + return -1; + } +#endif /* look for unmodified floating point conversion sequences */ _p0 = strstr(_fmt, "%e"); @@ -228,10 +297,35 @@ airSinglePrintf(FILE *file, char *str, size_t strSize, const char *_fmt, ...) { "is 3-character conv. seq." (but for TeemV2 we run with that type implication for "%g" and "%lg") */ if (isF || isD) { +#if defined(TEEM_HAS_PRINTF_L) +#define PRINT(F, S, C, V) \ + ((F) /* */ \ + ? fprintf_l((F), _aspCloc, (C), (V)) \ + : snprintf_l((S), strSize, _aspCloc, (C), (V))) +#elif defined(TEEM_HAS_PRINTF_L_WIN) +#define PRINT(F, S, C, V) \ + ((F) /* */ \ + ? _fprintf_l((F), (C), _aspCloc, (V)) \ + : _airSnprintfCLoc((S), strSize, (C), _aspCloc, (V))) +#else #define PRINT(F, S, C, V) \ ((F) /* */ \ ? fprintf((F), (C), (V)) \ : snprintf((S), strSize, (C), (V))) +#endif + /* the round-trip precision probe below must use the same "C" locale as PRINT, + so that the format it selects does not depend on the ambient LC_NUMERIC */ +#if defined(TEEM_HAS_PRINTF_L) +#define PROBE_SPRINT(B, BS, C, V) snprintf_l((B), (BS), _aspCloc, (C), (V)) +#define PROBE_STRTOD(B) strtod_l((B), NULL, _aspCloc) +#elif defined(TEEM_HAS_PRINTF_L_WIN) +#define PROBE_SPRINT(B, BS, C, V) _airSnprintfCLoc((B), (BS), (C), _aspCloc, (V)) +#define PROBE_STRTOD(B) _strtod_l((B), NULL, _aspCloc) +#else +/* TEEM_HAS_USELOCALE: this whole function already runs in the "C" locale */ +#define PROBE_SPRINT(B, BS, C, V) snprintf((B), (BS), (C), (V)) +#define PROBE_STRTOD(B) strtod((B), NULL) +#endif double val0; const char *_conv; size_t fmtSize; @@ -307,8 +401,8 @@ airSinglePrintf(FILE *file, char *str, size_t strSize, const char *_fmt, ...) { size_t buffSize = AIR_STRLEN_SMALL + 1; if (_p2) /* got "%g" intended for float */ { float fltval1, fltval0 = (float)val0; - snprintf(buff, buffSize, "%g", fltval0); - sscanf(buff, "%f", &fltval1); + PROBE_SPRINT(buff, buffSize, "%g", fltval0); + fltval1 = (float)PROBE_STRTOD(buff); if (fltval1 != fltval0) { /* "%g" by itself did NOT gave round-trip single-precision accuracy; assemble new format string with |%g|=2 --> |%.9g|=4 */ @@ -319,8 +413,8 @@ airSinglePrintf(FILE *file, char *str, size_t strSize, const char *_fmt, ...) { /* else "%g" gave round-trip single-precision accuracy; use `fmt` as is */ } else /* _p5: got "%lg% intended for double */ { double val1; - snprintf(buff, buffSize, "%g", val0); - sscanf(buff, "%lf", &val1); + PROBE_SPRINT(buff, buffSize, "%g", val0); + val1 = PROBE_STRTOD(buff); if (val1 != val0) { /* SORRY COPY PASTA */ shift2(conv + 3 /* first char after %lg */, fmt + strlen(fmt) - 1 /* last char in `fmt` */); @@ -337,14 +431,27 @@ airSinglePrintf(FILE *file, char *str, size_t strSize, const char *_fmt, ...) { } /* end of else !special */ free(fmt); #undef PRINT +#undef PROBE_SPRINT +#undef PROBE_STRTOD } else { /* `!isF && !isD`: conversion sequence is either for float or double but something more specific like "%.17g" (which NOTE does not get the above special treatment of IEEE754 special values!s), or, it isn't for any kind of floating point value. We can use the given `_fmt` as is (no local allocation of `fmt`) */ +#if defined(TEEM_HAS_PRINTF_L) + ret = file ? vfprintf_l(file, _aspCloc, _fmt, ap) /* */ + : vsnprintf_l(str, strSize, _aspCloc, _fmt, ap); +#elif defined(TEEM_HAS_PRINTF_L_WIN) + ret = file ? _vfprintf_l(file, _fmt, _aspCloc, ap) /* */ + : _airVsnprintfCLoc(str, strSize, _fmt, _aspCloc, ap); +#else ret = file ? vfprintf(file, _fmt, ap) : vsnprintf(str, strSize, _fmt, ap); +#endif } +#if defined(TEEM_HAS_USELOCALE) + uselocale(_aspOld); +#endif va_end(ap); return ret; } diff --git a/Modules/ThirdParty/NrrdIO/src/NrrdIO/parseAir.c b/Modules/ThirdParty/NrrdIO/src/NrrdIO/parseAir.c index 2c97ed9cbc6..7938570f498 100644 --- a/Modules/ThirdParty/NrrdIO/src/NrrdIO/parseAir.c +++ b/Modules/ThirdParty/NrrdIO/src/NrrdIO/parseAir.c @@ -23,7 +23,75 @@ 3. This notice may not be removed or altered from any source distribution. */ +/* Enable the POSIX-2008 thread-safe locale API (newlocale/strtod_l) where + available; must precede the first system-header include (NrrdIO.h pulls in + ). TEEM_HAS_STRTOD_L selects a locale-independent numeric parse + in airSingleSscanf(); platforms lacking it keep the historical sscanf(). */ +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) \ + || defined(__OpenBSD__) || defined(__DragonFly__) +# include +# include +# define TEEM_HAS_STRTOD_L 1 +#elif defined(__linux__) +/* __GLIBC__ comes from , not a compiler predefine, so it is unset + at this pre-include point; key off __linux__ (glibc, and musl >= 1.2.1, + provide newlocale/strtod_l under _GNU_SOURCE). */ +# ifndef _GNU_SOURCE +# define _GNU_SOURCE 1 +# endif +# include +# include +# define TEEM_HAS_STRTOD_L 1 +#elif defined(_MSC_VER) +# include +# include +# define TEEM_HAS_STRTOD_L_WIN 1 +#endif + #include "NrrdIO.h" +#include "privateAir.h" + +#if defined(TEEM_HAS_STRTOD_L) +/* Cached "C" locale for locale-independent strtod_l; created once. */ +static locale_t _airCLocale = (locale_t)0; +static pthread_once_t _airCLocaleOnce = PTHREAD_ONCE_INIT; +static void +_airCLocaleInit(void) { + _airCLocale = newlocale(LC_NUMERIC_MASK, "C", (locale_t)0); +} +static locale_t +_airGetCLocale(void) { + pthread_once(&_airCLocaleOnce, _airCLocaleInit); + return _airCLocale; +} +#elif defined(TEEM_HAS_STRTOD_L_WIN) +/* Cached "C" locale published exactly once via an atomic pointer CAS, so + concurrent first use is race-free: the losing thread frees its duplicate. */ +static void *volatile _airCLocale = NULL; +static _locale_t +_airGetCLocale(void) { + _locale_t loc = (_locale_t)_airCLocale; + if (!loc) { + _locale_t created = _create_locale(LC_NUMERIC, "C"); + void *prev = _InterlockedCompareExchangePointer(&_airCLocale, created, NULL); + if (prev) { _free_locale(created); loc = (_locale_t)prev; } + else { loc = created; } + } + return loc; +} +#endif + +/* Cached "C" LC_NUMERIC locale as an opaque pointer (locale_t or _locale_t); + NULL when the platform lacks the thread-safe locale API or creation failed. + Shared with airSinglePrintf (miscAir.c) via privateAir.h. */ +void * +air__CLocale(void) { +#if defined(TEEM_HAS_STRTOD_L) || defined(TEEM_HAS_STRTOD_L_WIN) + return (void *)_airGetCLocale(); +#else + return NULL; +#endif +} /* clang-format off */ static const char * @@ -105,12 +173,51 @@ airSingleSscanf(const char *str, const char *fmt, void *ptr) { } else if (strstr(tmp, "inf")) { val = (double)AIR_POS_INF; } else { - /* nothing special matched; pass it off to sscanf() */ - /* (save setlocale here) */ + /* nothing special matched; parse one floating-point value with a + locale-independent "C" parse so NRRD's '.' decimal separator is + honored regardless of the process/thread LC_NUMERIC setting. */ +#if defined(TEEM_HAS_STRTOD_L) || defined(TEEM_HAS_STRTOD_L_WIN) + char *endptr = NULL; + double dval = 0.0; + ret = 0; + /* a null "C" locale means we cannot parse locale-independently: + fail the parse rather than risk a silently wrong value */ +# if defined(TEEM_HAS_STRTOD_L) + locale_t cloc = _airGetCLocale(); + if (cloc) { + errno = 0; + dval = strtod_l(str, &endptr, cloc); +# else + _locale_t cloc = _airGetCLocale(); + if (cloc) { + errno = 0; + dval = _strtod_l(str, &endptr, cloc); +# endif + /* accept a conversion unless it overflowed (ERANGE with +/-HUGE_VAL); + ERANGE underflow is a valid subnormal or zero */ + if (endptr != str + && !(ERANGE == errno && (HUGE_VAL == dval || -HUGE_VAL == dval))) { + ret = 1; + } + } + /* in double range but overflows the requested float: AIR_FLOAT would + narrow to infinity, which is the same silent wrong value that the + ERANGE test above rejects */ + if (ret && fmt[1] != 'l' && (dval > FLT_MAX || dval < -FLT_MAX)) { + ret = 0; + } + if (ret) { + if (fmt[1] == 'l') { *((double *)(ptr)) = dval; } + else { *((float *)(ptr)) = AIR_FLOAT(dval); } + } + free(tmp); + return ret; +#else + /* fallback: historical locale-dependent behavior, unchanged */ ret = sscanf(str, fmt, ptr); - /* (return setlocale here) */ free(tmp); return ret; +#endif } /* else we matched "nan", "-inf", or "inf"; now set val accordingly */ if (fmt[1] == 'l') { diff --git a/Modules/ThirdParty/NrrdIO/src/NrrdIO/privateAir.h b/Modules/ThirdParty/NrrdIO/src/NrrdIO/privateAir.h index 4b91589d356..282f8c21479 100644 --- a/Modules/ThirdParty/NrrdIO/src/NrrdIO/privateAir.h +++ b/Modules/ThirdParty/NrrdIO/src/NrrdIO/privateAir.h @@ -55,6 +55,10 @@ typedef unsigned int uint; /* uint is just more concise */ /* miscAir.c */ extern double air__SanityHelper(double val); +/* parseAir.c: cached "C" LC_NUMERIC locale as an opaque pointer (locale_t or + _locale_t); NULL when unavailable. For locale-independent numeric I/O. */ +extern void *air__CLocale(void); + #ifdef __cplusplus } #endif