Fold case invariantly in regex matching - #104
Merged
Merged
Conversation
RegexOptions.IgnoreCase without RegexOptions.CultureInvariant folds case
using the calling thread's CurrentCulture. Under tr-TR that stops "i" and
"I" being the same letter, so a filter of "img" no longer matched
"IMG_1234.JPG".
The regex cache is keyed by pattern and sensitivity, not by culture, so the
fault was not confined to Turkish callers: whichever culture compiled a
pattern first decided the answer for every later caller on every thread.
The glob path already folds invariantly, so the two paths now agree.
The test project opts out of the InvariantGlobalization default ktsu.Sdk
sets, which is what kept this invisible: with every culture resolving to the
invariant one, new CultureInfo("tr-TR") throws and the fold under test can
never happen. That default belongs to the application being built, not to
the applications that consume this library.
Fixes #102
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012betVHk3gFcj5RYkEe4vrm
|
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.



Fixes #102
The defect
RegexOptions.IgnoreCasewithoutRegexOptions.CultureInvariantfolds case using the calling thread'sCurrentCulture. Undertr-TR,iandIare not each other's case pair —iuppercases toİandIlowercases toı.Measured on .NET 10.0.401, Linux:
tr-TRnew Regex("i", IgnoreCase).IsMatch("I")new Regex("I", IgnoreCase).IsMatch("i")new Regex("img", IgnoreCase).IsMatch("IMG_1234.JPG")new Regex("i", IgnoreCase | CultureInvariant).IsMatch("I")new Regex(@".*\.jpg", IgnoreCase).IsMatch("IMG_1234.JPG")That last row is why this was never caught. The existing
RegexMatchesAcrossCaseWhenCaseInsensitiveIsRequesteduses.*\.jpg, andj,pandgfold identically in every culture. Only a pattern containingiorIshows the defect.The cache makes it worse than a Turkish-locale bug.
RegexCacheis keyed byCacheKey(filter, caseSensitivity)— pattern and sensitivity, not culture. A pattern compiled once is reused for every later caller on every thread, so whichever culture happened to get there first decides the answer for the whole process. A single request served on a thread with a Turkish culture poisons the entry for everyone.The change
One option added at
TextFilter.cs:412, as the issue suggests:This brings the regex path into line with the glob path, which
DotNet.Globalready folds invariantly — so the two filter types now agree rather than disagreeing on a locale.Why nobody noticed: the test project could not reproduce it
This is the part worth reading before reviewing the diff.
ktsu.Sdk/Sdk.props:738sets<InvariantGlobalization>true</InvariantGlobalization>for every project built with the SDK. Under that setting every culture request resolves to the invariant culture andnew CultureInfo("tr-TR")throwsCultureNotFoundExceptionoutright. A test written against the tr-TR fold does not fail — it errors, and no test written the obvious way could ever have caught this.So
TextFilter.Test.csprojnow sets<InvariantGlobalization>false</InvariantGlobalization>, with a comment explaining why.That is a deliberate judgement and the one point in this change I would push back on myself, so the reasoning in full: the SDK default is right for an application, which controls its own runtime and can decide it needs no culture data. It is wrong for the test host of a library, because the library's consumers are not necessarily built with ktsu.Sdk, and a consumer with real culture data is precisely the caller this bug afflicts. The test project's job is to stand in for those callers, which it cannot do while stripped of the culture data they have.
Nothing about the shipping library changes —
InvariantGlobalizationis a runtime host setting, it affects onlyTextFilter.Test's own test host, andTextFilter.csprojis untouched. The whole existing suite passes identically under it (84 before, 84 after, plus the 2 new), so turning real culture data on did not perturb any other test.Tests
RegexCaseInsensitivityDoesNotDependOnTheCurrentCultureimgagainstIMG_5678.JPGundertr-TRARegexCompiledUnderOneCultureAnswersTheSameUnderAnothertr-TRthen invariant must agreeBoth use patterns used nowhere else in the suite, so each owns its cache entry and the ordering the test asserts on is the ordering that actually runs. Both restore
CurrentCulturein afinally.The first carries an
Assert.Inconclusiveguard onCultureInfo.CurrentCulture.TextInfo.ToUpper("i") == "I". If someone later re-enables invariant globalization on the test project, the test says so out loud instead of passing for the wrong reason. (The guard is written without aRegexbecause this repo runs analyzers as errors andSYSLIB1045demands[GeneratedRegex]for any inline pattern.)Proved failing without the fix. Reverting only
TextFilter/TextFilter.csand keeping everything else:Both fail on the fold itself, not on a message that merely changed shape.
Verification
dotnet build TextFilter.sln -c Release— succeeded, 0 warnings, 0 errors (analyzers run as errors here)dotnet test TextFilter.sln -c Release— 86 total, 86 passed, 0 failed, 0 skippedTextFilter.cs— 2 of 86 failed, as aboveDocs
README.md's "Case Sensitivity" section gains a paragraph stating that case folds invariantly and naming the dotted/dotless I as the reason. The issue notes that neither the README nor the tests mentioned culture, which is what made this look like settled behaviour rather than an oversight.Not in this change
ktsu-dev/TextFilter#103—RegexCacheandGlobCachegrowing unbounded for the process lifetime — is untouched. It is about the same two caches and is genuinely adjacent, but it is an eviction-policy decision rather than a correctness fix, and folding it in here would put a bounded-cache design in a one-option bug fix.🤖 Generated with Claude Code
https://claude.ai/code/session_012betVHk3gFcj5RYkEe4vrm
Generated by Claude Code