From f908c911a16f94737477872c54e79316cb99cf7f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 23:30:20 +0200 Subject: [PATCH 1/2] test(regex): isolate ASCII-strict negative sibling folds Add a system-Perl-first reducer for negated classes that must exclude safe non-ASCII fold siblings under /aa while retaining ASCII and unrelated members. Generated with [OpenAI Codex](https://openai.com/codex) Co-Authored-By: OpenAI Codex --- .../joni_ascii_strict_negative_sibling_fold.t | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/test/resources/unit/regex/joni_ascii_strict_negative_sibling_fold.t diff --git a/src/test/resources/unit/regex/joni_ascii_strict_negative_sibling_fold.t b/src/test/resources/unit/regex/joni_ascii_strict_negative_sibling_fold.t new file mode 100644 index 000000000..30bffbba5 --- /dev/null +++ b/src/test/resources/unit/regex/joni_ascii_strict_negative_sibling_fold.t @@ -0,0 +1,31 @@ +use strict; +use warnings; +use utf8; +use Test::More tests => 12; + +ok("\x{1E9E}" !~ /^[^\x{00DF}]$/iaa, + 'aa negative class excludes capital sharp s sibling'); +ok("\x{00DF}" !~ /^[^\x{1E9E}]$/iaa, + 'aa negative class excludes lowercase sharp s sibling'); +ok("\x{FB06}" !~ /^[^\x{FB05}]$/iaa, + 'aa negative class excludes second st ligature sibling'); +ok("\x{FB05}" !~ /^[^\x{FB06}]$/iaa, + 'aa negative class excludes first st ligature sibling'); + +ok("s" =~ /^[^\x{00DF}]$/iaa, + 'aa negative sharp s class retains ASCII s'); +ok("ä" =~ /^[^\x{00DF}]$/iaa, + 'aa negative sharp s class retains unrelated non-ASCII'); +ok("s" =~ /^[^\x{FB05}]$/iaa, + 'aa negative ligature class retains ASCII s'); +ok("ä" =~ /^[^\x{FB05}]$/iaa, + 'aa negative ligature class retains unrelated non-ASCII'); + +ok("x\x{1E9E}y" !~ /^x(?iaa:[^\x{00DF}])y$/u, + 'scoped aa negative class excludes sharp s sibling'); +ok("xäy" =~ /^x(?iaa:[^\x{00DF}])y$/u, + 'scoped aa negative class retains unrelated member'); +ok("_\x{FB06}_" !~ /^_(?iaa:[^\x{FB05}])_$/u, + 'anchored aa negative class excludes ligature sibling'); +ok("_ä_" =~ /^_(?iaa:[^\x{FB05}])_$/u, + 'anchored aa negative class retains unrelated member'); From 54ce9befce4e7168c5e72bc967caad69a1b36e8b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 19 Aug 2026 00:11:03 +0200 Subject: [PATCH 2/2] fix(regex): retain safe strict folds in negative classes Enumerate complete character-class fold relations, then reject strict-mode callbacks whenever any output code point crosses the ASCII boundary. Generated with [OpenAI Codex](https://openai.com/codex) Co-Authored-By: OpenAI Codex --- .../joni/src/org/joni/ApplyCaseFold.java | 11 ++- .../joni/src/org/joni/ScanEnvironment.java | 7 +- ...estPerlAsciiStrictNegativeSafeSibling.java | 67 +++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 third_party/joni/test/org/joni/test/TestPerlAsciiStrictNegativeSafeSibling.java diff --git a/third_party/joni/src/org/joni/ApplyCaseFold.java b/third_party/joni/src/org/joni/ApplyCaseFold.java index 59021b4c8..a4122684e 100644 --- a/third_party/joni/src/org/joni/ApplyCaseFold.java +++ b/third_party/joni/src/org/joni/ApplyCaseFold.java @@ -41,7 +41,7 @@ public void apply(int from, int[]to, int length, Object o) { boolean addFlag; if (Option.isPerlAsciiStrict(env.option) - && Encoding.isAscii(from) != Encoding.isAscii(to[0])) { + && perlAsciiStrictRelationCrossesAscii(from, to, length)) { return; } @@ -111,6 +111,15 @@ public void apply(int from, int[]to, int length, Object o) { } + private static boolean perlAsciiStrictRelationCrossesAscii(int from, int[] to, + int length) { + boolean fromAscii = Encoding.isAscii(from); + for (int i = 0; i < length; i++) { + if (fromAscii != Encoding.isAscii(to[i])) return true; + } + return false; + } + static boolean isEligible(CClassNode ascCc, Encoding enc, int code) { if (ascCc == null) return false; boolean eligible = ascCc.isCodeInCC(enc, code); diff --git a/third_party/joni/src/org/joni/ScanEnvironment.java b/third_party/joni/src/org/joni/ScanEnvironment.java index cc88fc071..4405782f6 100644 --- a/third_party/joni/src/org/joni/ScanEnvironment.java +++ b/third_party/joni/src/org/joni/ScanEnvironment.java @@ -68,9 +68,10 @@ public final class ScanEnvironment { } int caseFoldFlagFor(int option) { - return Option.isPerlAsciiStrict(option) - ? caseFoldFlag & ~Config.INTERNAL_ENC_CASE_FOLD_MULTI_CHAR - : caseFoldFlag; + // Character classes need the complete fold relation so + // ApplyCaseFold can retain safe non-ASCII siblings under /aa while + // filtering relations that cross into ASCII. + return caseFoldFlag; } int addMemEntry() { diff --git a/third_party/joni/test/org/joni/test/TestPerlAsciiStrictNegativeSafeSibling.java b/third_party/joni/test/org/joni/test/TestPerlAsciiStrictNegativeSafeSibling.java new file mode 100644 index 000000000..c3ee7b021 --- /dev/null +++ b/third_party/joni/test/org/joni/test/TestPerlAsciiStrictNegativeSafeSibling.java @@ -0,0 +1,67 @@ +/* + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.joni.test; + +import static org.junit.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; + +import org.jcodings.specific.UTF8Encoding; +import org.joni.Option; +import org.joni.Regex; +import org.joni.Syntax; +import org.junit.Test; + +public class TestPerlAsciiStrictNegativeSafeSibling { + private static int search(String pattern, String input) { + byte[] patternBytes = pattern.getBytes(StandardCharsets.UTF_8); + byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); + Regex regex = new Regex(patternBytes, 0, patternBytes.length, + Option.IGNORECASE | Option.PERL_ASCII_STRICT, + UTF8Encoding.INSTANCE, Syntax.PerlNG); + return regex.matcher(inputBytes).search(0, inputBytes.length, Option.NONE); + } + + private static void matches(String pattern, String input) { + assertEquals(0, search(pattern, input)); + } + + private static void misses(String pattern, String input) { + assertEquals(-1, search(pattern, input)); + } + + @Test + public void appliesSafeSiblingsToOrdinaryPositiveAndNegativeClasses() { + matches("^[ß]$", "ẞ"); + misses("^[^ß]$", "ẞ"); + matches("^[ſt]$", "st"); + misses("^[^ſt]$", "st"); + matches("^[^ß]$", "ä"); + matches("^[^ſt]$", "ä"); + } + + @Test + public void rejectsAsciiAtEveryPositionOfAMultiCharacterFold() { + misses("^[ʼn]$", "ʼn"); + misses("^[ʼn]$", "ʼN"); + matches("^[ʼn]$", "ʼn"); + matches("^[^ʼn]$", "ʼ"); + } +}