Skip to content

Make simpleFold follow only symmetric case mappings. - #213

Open
jemmix wants to merge 1 commit into
google:masterfrom
jemmix:fix-simple-fold-asymmetric-mappings
Open

jemmix wants to merge 1 commit into
google:masterfrom
jemmix:fix-simple-fold-asymmetric-mappings

Conversation

@jemmix

@jemmix jemmix commented Sep 17, 2026

Copy link
Copy Markdown

Fixes #168.

As diagnosed in #168 (see @rsc's comment): simpleFold's table-miss fallback follows toLower/toUpper assuming they form closed two-element orbits with the rune. That holds only for symmetric pairs — for runes whose asymmetric case mappings postdate the generated tables (U+1C80..U+1C88, Unicode 9.0 vs tables at 6.0) the walk steps into the partner's own orbit and never cycles back, hanging Pattern.compile and the match-time fold walks (minFoldRune, appendFoldedRange, equalsIgnoreCase).

This implements the symmetry check suggested there: the fallback follows a mapping only when the partner maps back (toLower(r) is followed iff toUpper(toLower(r)) == r, and vice versa), so such runes are fold-inert and every walk terminates structurally. Control-flow only, zero data changes; symmetric pairs — including post-6.0 ones reached via the runtime's mappings — fold exactly as before.

Regenerating the tables from current Unicode (or the runtime-derived orbit computation also discussed in #168) is the fuller fix and would restore folding for these runes; this change is deliberately minimal and also guards whatever skew a future regeneration misses, since termination no longer depends on the tables agreeing with the runtime.

Adds CaseFoldTerminationTest: every U+1C80..U+1C88 compiles under (?i) (all used to hang), the letters are consistently fold-inert in literal and class form in both directions (e.g. (?i)Ꚁ no longer matches В/в), and symmetric pairs keep folding — plain two-element pairs via the fallback plus the three-element K/k/Kelvin-sign orbit from the table.

Verified locally with ./gradlew check on JDK 8: 1829 tests, 0 failures, verifyGoogleJavaFormat + license green.

Disclosure: found while fuzzing https://github.com/jemmix/tdfa-jvm against re2j (patch 0003 there); analysis and patch composed agentically with GLM 5.3 assistance; verification run locally by me.

simpleFold's table-miss fallback follows toLower/toUpper, assuming the
mappings form closed two-element orbits with the rune. That holds only
for symmetric pairs: runes whose asymmetric case mappings postdate the
generated tables' Unicode version (e.g. U+1C80..U+1C88, Cyrillic
historic letters, added in Unicode 9.0 vs tables at 6.0) step into the
partner's own orbit and never cycle back, hanging Pattern.compile and
the match-time fold walks (minFoldRune, appendFoldedRange,
equalsIgnoreCase).

The fallback now follows a mapping only when the partner maps back
(a verified symmetric pair), so such runes are fold-inert and every
walk terminates structurally -- no hop cap, control-flow only, zero
data changes. Symmetric pairs, including post-6.0 ones reached via the
runtime's mappings, fold exactly as before.

Adds CaseFoldTerminationTest: every U+1C80..U+1C88 compiles under (?i)
(used to hang), the letters are consistently fold-inert in literal and
class form in both directions, and symmetric pairs (z/Z, Cyrillic
two-element pairs, the three-element K/k/Kelvin-sign orbit) keep
folding.

Regenerating the tables from current Unicode is the fuller fix (the
universe then matches the runtime again); this change makes every walk
terminate regardless of table/runtime skew, so it also guards whatever
skew the next regeneration misses.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Infinite loop in Pattern#compile on certain case-insensitive patterns

1 participant