Skip to content

Commit 1b2fbfc

Browse files
committed
Fix Repeat off-by-one error
Adjust the upper bound of the Range down when forming the quantification, as it expects an inclusive upper bound.
1 parent b7bfd16 commit 1b2fbfc

File tree

2 files changed

+128
-16
lines changed

2 files changed

+128
-16
lines changed

Sources/_StringProcessing/Regex/DSLTree.swift

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -856,22 +856,35 @@ extension DSLTree.Node {
856856
assert(range.lowerBound >= 0, "Cannot specify a negative lower bound")
857857
assert(!range.isEmpty, "Cannot specify an empty range")
858858

859-
let kind: DSLTree.QuantificationKind = behavior.map { .explicit($0.dslTreeKind) } ?? .default
860-
861-
switch (range.lowerBound, range.upperBound) {
862-
case (0, Int.max): // 0...
863-
return .quantification(.zeroOrMore, kind, node)
864-
case (1, Int.max): // 1...
865-
return .quantification(.oneOrMore, kind, node)
866-
case _ where range.count == 1: // ..<1 or ...0 or any range with count == 1
859+
let kind: DSLTree.QuantificationKind = behavior
860+
.map { .explicit($0.dslTreeKind) } ?? .default
861+
862+
// The upper bound needs adjusting down as
863+
// `.quantification` expects a closed range.
864+
let lower = range.lowerBound
865+
let upperInclusive = range.upperBound - 1
866+
867+
// Unbounded cases
868+
if range.upperBound == Int.max {
869+
switch lower {
870+
case 0: // 0...
871+
return .quantification(.zeroOrMore, kind, node)
872+
case 1: // 1...
873+
return .quantification(.oneOrMore, kind, node)
874+
default: // n...
875+
return .quantification(.nOrMore(lower), kind, node)
876+
}
877+
}
878+
if range.count == 1 {
879+
// ..<1 or ...0 or any range with count == 1
867880
// Note: `behavior` is ignored in this case
868-
return .quantification(.exactly(range.lowerBound), .default, node)
869-
case (0, _): // 0..<n or 0...n or ..<n or ...n
870-
return .quantification(.upToN(range.upperBound), kind, node)
871-
case (_, Int.max): // n...
872-
return .quantification(.nOrMore(range.lowerBound), kind, node)
873-
default: // any other range
874-
return .quantification(.range(range.lowerBound, range.upperBound), kind, node)
881+
return .quantification(.exactly(lower), .default, node)
882+
}
883+
switch lower {
884+
case 0: // 0..<n or 0...n or ..<n or ...n
885+
return .quantification(.upToN(upperInclusive), kind, node)
886+
default:
887+
return .quantification(.range(lower, upperInclusive), kind, node)
875888
}
876889
}
877890
}

Tests/RegexBuilderTests/RegexDSLTests.swift

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,13 @@ class RegexDSLTests: XCTestCase {
450450

451451
try _testDSLCaptures(
452452
("aaabbbcccdddeeefff", "aaabbbcccdddeeefff"),
453+
("aaabbbcccccdddeeefff", "aaabbbcccccdddeeefff"),
454+
("aaabbbcccddddeeefff", "aaabbbcccddddeeefff"),
455+
("aaabbbccccccdddeeefff", nil),
453456
("aaaabbbcccdddeeefff", nil),
454457
("aaacccdddeeefff", nil),
455458
("aaabbbcccccccdddeeefff", nil),
459+
("aaabbbcccdddddeeefff", nil),
456460
("aaabbbcccddddddeeefff", nil),
457461
("aaabbbcccdddefff", nil),
458462
("aaabbbcccdddeee", "aaabbbcccdddeee"),
@@ -465,7 +469,102 @@ class RegexDSLTests: XCTestCase {
465469
Repeat(2...) { "e" }
466470
Repeat(0...) { "f" }
467471
}
468-
472+
473+
try _testDSLCaptures(
474+
("", nil),
475+
("a", nil),
476+
("aa", "aa"),
477+
("aaa", "aaa"),
478+
matchType: Substring.self, ==)
479+
{
480+
Repeat(2...) { "a" }
481+
}
482+
483+
try _testDSLCaptures(
484+
("", ""),
485+
("a", "a"),
486+
("aa", "aa"),
487+
("aaa", nil),
488+
matchType: Substring.self, ==)
489+
{
490+
Repeat(...2) { "a" }
491+
}
492+
493+
try _testDSLCaptures(
494+
("", ""),
495+
("a", "a"),
496+
("aa", nil),
497+
("aaa", nil),
498+
matchType: Substring.self, ==)
499+
{
500+
Repeat(..<2) { "a" }
501+
}
502+
503+
try _testDSLCaptures(
504+
("", ""),
505+
("a", nil),
506+
("aa", nil),
507+
matchType: Substring.self, ==)
508+
{
509+
Repeat(...0) { "a" }
510+
}
511+
512+
try _testDSLCaptures(
513+
("", ""),
514+
("a", nil),
515+
("aa", nil),
516+
matchType: Substring.self, ==)
517+
{
518+
Repeat(0 ... 0) { "a" }
519+
}
520+
521+
try _testDSLCaptures(
522+
("", ""),
523+
("a", "a"),
524+
("aa", nil),
525+
matchType: Substring.self, ==)
526+
{
527+
Repeat(0 ... 1) { "a" }
528+
}
529+
530+
try _testDSLCaptures(
531+
("", nil),
532+
("a", "a"),
533+
("aa", "aa"),
534+
("aaa", nil),
535+
matchType: Substring.self, ==)
536+
{
537+
Repeat(1 ... 2) { "a" }
538+
}
539+
540+
try _testDSLCaptures(
541+
("", ""),
542+
("a", nil),
543+
("aa", nil),
544+
matchType: Substring.self, ==)
545+
{
546+
Repeat(0 ..< 1) { "a" }
547+
}
548+
549+
try _testDSLCaptures(
550+
("", ""),
551+
("a", "a"),
552+
("aa", nil),
553+
matchType: Substring.self, ==)
554+
{
555+
Repeat(0 ..< 2) { "a" }
556+
}
557+
558+
try _testDSLCaptures(
559+
("", nil),
560+
("a", "a"),
561+
("aa", "aa"),
562+
("aaa", nil),
563+
matchType: Substring.self, ==)
564+
{
565+
Repeat(1 ..< 3) { "a" }
566+
}
567+
469568
let octoDecimalRegex: Regex<(Substring, Int?)> = Regex {
470569
let charClass = CharacterClass(.digit, "a"..."h")//.ignoringCase()
471570
Capture {

0 commit comments

Comments
 (0)