From 1c19d6d4feedb57b25cb1711bb44ef6d8d460d02 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 5 Oct 2026 01:41:32 -0500 Subject: [PATCH] Fix optimized ASCII case-insensitive matching In AsciiBitset, adding a character with case insensitivity enabled uses the wrong lower bound: 64, which is `@`, instead of 65 (`A`). --- .../Utility/AsciiBitset.swift | 2 +- Tests/RegexTests/MatchTests.swift | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Sources/_StringProcessing/Utility/AsciiBitset.swift b/Sources/_StringProcessing/Utility/AsciiBitset.swift index 61a6a6891..67f5f3937 100644 --- a/Sources/_StringProcessing/Utility/AsciiBitset.swift +++ b/Sources/_StringProcessing/Utility/AsciiBitset.swift @@ -46,7 +46,7 @@ extension DSLTree.CustomCharacterClass { setBit(val) if isCaseInsensitive { switch val { - case 64...90: setBit(val + 32) + case 65...90: setBit(val + 32) case 97...122: setBit(val - 32) default: break } diff --git a/Tests/RegexTests/MatchTests.swift b/Tests/RegexTests/MatchTests.swift index d75dbde64..2d9a6b1c2 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -1458,6 +1458,29 @@ extension RegexTests { firstMatchTest( "(?i)[X-cD]", input: input, match: input, semanticLevel: semantics) } + + // Boundary tests for case insensitive ASCII matching. + firstMatchTest( + "(?i)[@]", input: "@", match: "@", semanticLevel: semantics) + firstMatchTest( + "(?i)[@]", input: "`", match: nil, semanticLevel: semantics) + firstMatchTest( + "(?i)[`]", input: "`", match: "`", semanticLevel: semantics) + firstMatchTest( + "(?i)[`]", input: "@", match: nil, semanticLevel: semantics) + // A range that spans the "@"/"A" boundary folds "A" but not "@". + firstMatchTest( + "(?i)[?-A]", input: "a", match: "a", semanticLevel: semantics) + firstMatchTest( + "(?i)[?-A]", input: "`", match: nil, semanticLevel: semantics) + // The letter ends of each range still fold. + for (pattern, input) in [ + ("(?i)[A]", "a"), ("(?i)[a]", "A"), + ("(?i)[Z]", "z"), ("(?i)[z]", "Z"), + ] { + firstMatchTest( + pattern, input: input, match: input, semanticLevel: semantics) + } } }