BUG: Locale-independent float parsing and printing in NRRD/DICOM IO - #6695
Open
hjmjohnson wants to merge 2 commits into
Open
BUG: Locale-independent float parsing and printing in NRRD/DICOM IO#6695hjmjohnson wants to merge 2 commits into
hjmjohnson wants to merge 2 commits into
Conversation
hjmjohnson
force-pushed
the
nrrdio-locale-strtod
branch
from
July 24, 2026 00:43
7ffe114 to
2aa5799
Compare
hjmjohnson
marked this pull request as ready for review
July 24, 2026 00:52
This comment was marked as resolved.
This comment was marked as resolved.
hjmjohnson
marked this pull request as draft
July 24, 2026 15:54
hjmjohnson
force-pushed
the
nrrdio-locale-strtod
branch
from
July 24, 2026 16:11
2aa5799 to
835c21a
Compare
Member
Author
|
Force-push |
hjmjohnson
marked this pull request as ready for review
July 24, 2026 17:01
hjmjohnson
marked this pull request as draft
July 24, 2026 18:50
NRRD float I/O honored the ambient LC_NUMERIC: under a decimal-comma locale "0.878906" parsed as 0 and the writer emitted "0,878906", corrupting spacings, directions, origins, and ASCII pixel values. Parse, print, and the precision probe share one cached "C" locale (privateAir.h): strtod_l() and the printf_l() family, or a uselocale() swap on glibc/musl, which lack printf_l. A parse fails on overflow (ERANGE with +/-HUGE_VAL, or a value past FLT_MAX for a "%f" target) or if the "C" locale cannot be created; ERANGE underflow is a valid subnormal or zero. Mirrors the teem branch locale-independent-numeric-parse.
itk::StringToDouble and itk::StringToFloat followed the ambient LC_NUMERIC, so every migrated IO call site parsed "0.878906" as 0 under a decimal-comma locale. They parse with strtod_l/strtof_l against a cached "C" locale, keeping the itk::ExceptionObject contract; only overflow throws, as ERANGE underflow is a valid subnormal or zero. A platform lacking newlocale and _configthreadlocale fails to compile. NumericLocale is removed: uselocale() cannot bind the non-_l std::strtod on macOS 13 libc, and it shipped only in the v6.0b02 beta, so no deprecation shim is required.
hjmjohnson
force-pushed
the
nrrdio-locale-strtod
branch
from
July 24, 2026 19:59
835c21a to
754af0f
Compare
hjmjohnson
marked this pull request as ready for review
July 24, 2026 20:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Starts from the red
NumericLocale.WorksWithDifferentInitialLocalenightly on open.cdash.org (Mac13.x-AppleClang-dbg-Universal). NRRD and DICOM float I/O now parses and prints against a cached "C" locale; the beta-onlyNumericLocaleis removed.Motivating dashboard failure
This work began from a persistently red nightly on the Insight dashboard at open.cdash.org, not from a code review:
RogueResearch24Mac13.x-AppleClang-dbg-UniversalNumericLocale.WorksWithDifferentInitialLocaleThe test asserted that the
NumericLocaleRAII guard makes numeric parsing locale-independent. It failed only on macOS 13, because that libc's non-_lstrtodignores the thread-local locale thatuselocale()installs — so the guard could not honor its contract there, and the test was asserting something the mechanism could not deliver on that platform. Investigating why the guard did not work exposed the larger problem: NrrdIO's read and write paths were locale-dependent underneath it, and on Linux the guard had been masking that from CI. Hence the fix replaces the mechanism rather than adjusting the test.(Build identifiers are recorded rather than linked: CDash build URLs expire, so a
buildIdlink would rot while this PR's history does not.)Root cause
Under a decimal-comma
LC_NUMERIC(e.g.de_DE.UTF-8),"0.878906"parses as0and prints as"0,878906":airSingleSscanf()→sscanf()in vendored NrrdIO (spacings, directions, origins, ASCII pixels).airSinglePrintf()→printf-family — the writer emitted comma decimals, persisting corrupt files.itk::StringTo*IO call site:std::stod/std::stoffollow the ambient locale.NumericLocaleRAII guard switching the thread-local locale viauselocale(), is silently ignored by the non-_lstrtod/printfon macOS 13-era libc — and on Linux it masked the NrrdIO bug from CI.Fix (2 commits)
Commit 1 — vendored NrrdIO reads and writes locale-independently.
airSingleSscanf()parses viastrtod_l;airSinglePrintf()prints viafprintf_l/snprintf_l(BSD/macOS),_fprintf_l(Windows), or auselocale()swap (glibc/musl, which lackprintf_lbut whoseprintfhonors the thread-local locale). The internal round-trip precision probe uses that same "C" locale on every platform arm, so the format it selects never depends on the ambient locale. One cached "C" locale is shared viaprivateAir.h(air__CLocale()).A parse fails rather than yielding a wrong value on overflow —
ERANGEwith ±HUGE_VAL, or a double-range value pastFLT_MAXfor a"%f"target, which would otherwise narrow silently to infinity — and when the "C" locale is unobtainable.ERANGEunderflow is a valid subnormal or zero and is accepted.On Windows the string path goes through helpers that restore the C99
snprintfcontract (always terminate, return the would-be length via_vscprintf_l), which_snprintf_lalone does not honor on truncation; the rest ofnrrdSprint[]relies on that contract.The now-redundant RAII guards in
itk::NrrdImageIOare removed.Commit 2 — canonical helpers fixed,
NumericLocaleremoved.itk::StringToDouble/StringToFloat(itkStringConvert.cxx, the #6089itk::ExceptionObject-wrapping helpers) parse viastrtod_l/strtof_l, so every existing IO call site is fixed at once. A platform providing neithernewlocalenor_configthreadlocaleis a#error, so there is no silent fall-back to an ambient-locale parse.DCMTKTransformIOusesitk::StringToDoublefor DICOM DS matrix entries.NumericLocaleis deleted outright — it appeared only in thev6.0b02beta, so no deprecation shim is required. Thenewlocale/_configthreadlocaletry_compile checks remain, now servingitkStringConvert.Upstream teem
The identical NrrdIO change (parse + print + shared locale + native test
src/air/test/locale.c) is on the teem branchlocale-independent-numeric-parse(SourceForgessh://git.code.sf.net/p/teem/teem.git, commit3cbb824ed). This PR carries it in the vendored copy so the fix ships now; when teem merges,UpdateFromUpstream.shre-vendors cleanly. Teem builds clean and its locale test passes under 4 decimal-comma locales, covering parse, print round trips, and theFLT_MAXoverflow case.Tests
A shared
itk::testhelper (Modules/Core/TestKernel/include/itkTestNumericLocale.h) carries the candidate locale names in POSIX and Windows spelling plus the "is a comma locale guaranteed here?" policy, so both tests agree on it.itkStringConvertGTest: locale-independence under the comma locales, with the overflow/underflow contract re-asserted inside the comma-locale loop, plus a"0,878906"→0.0check that positively confirms the "C" locale is in effect.itkNrrdLocaleTest: reads verified under each available comma locale, plus a write-under-comma-locale round trip.locale-genruns.NumericLocaletests removed with the class.Verified locally on macOS (arm64): full ITK build 3730 targets / 0 errors;
ITKCommon+ITKIONRRD166/166;ITKIODCMTK+IOTransformDCMTK50/50; teem build + locale test green. Windows print paths rely on CI.