From 1b2fbfc6c89c46030550529edbfc666b2157e237 Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Fri, 1 Jul 2022 12:58:00 +0100 Subject: [PATCH 1/2] 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. --- Sources/_StringProcessing/Regex/DSLTree.swift | 43 +++++--- Tests/RegexBuilderTests/RegexDSLTests.swift | 101 +++++++++++++++++- 2 files changed, 128 insertions(+), 16 deletions(-) diff --git a/Sources/_StringProcessing/Regex/DSLTree.swift b/Sources/_StringProcessing/Regex/DSLTree.swift index 196927803..bcb1c690f 100644 --- a/Sources/_StringProcessing/Regex/DSLTree.swift +++ b/Sources/_StringProcessing/Regex/DSLTree.swift @@ -856,22 +856,35 @@ extension DSLTree.Node { assert(range.lowerBound >= 0, "Cannot specify a negative lower bound") assert(!range.isEmpty, "Cannot specify an empty range") - let kind: DSLTree.QuantificationKind = behavior.map { .explicit($0.dslTreeKind) } ?? .default - - switch (range.lowerBound, range.upperBound) { - case (0, Int.max): // 0... - return .quantification(.zeroOrMore, kind, node) - case (1, Int.max): // 1... - return .quantification(.oneOrMore, kind, node) - case _ where range.count == 1: // ..<1 or ...0 or any range with count == 1 + let kind: DSLTree.QuantificationKind = behavior + .map { .explicit($0.dslTreeKind) } ?? .default + + // The upper bound needs adjusting down as + // `.quantification` expects a closed range. + let lower = range.lowerBound + let upperInclusive = range.upperBound - 1 + + // Unbounded cases + if range.upperBound == Int.max { + switch lower { + case 0: // 0... + return .quantification(.zeroOrMore, kind, node) + case 1: // 1... + return .quantification(.oneOrMore, kind, node) + default: // n... + return .quantification(.nOrMore(lower), kind, node) + } + } + if range.count == 1 { + // ..<1 or ...0 or any range with count == 1 // Note: `behavior` is ignored in this case - return .quantification(.exactly(range.lowerBound), .default, node) - case (0, _): // 0.. = Regex { let charClass = CharacterClass(.digit, "a"..."h")//.ignoringCase() Capture { From 08e12b99111cdf0de3630c5d6fdde94541586419 Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Fri, 1 Jul 2022 14:36:45 +0100 Subject: [PATCH 2/2] Upgrade a couple of assertions to preconditions Seems like we ought to crash in a release build instead of potentially crashing during byte code emission or silently failing to match. --- Sources/RegexBuilder/Variadics.swift | 44 +++++++++---------- .../VariadicsGenerator.swift | 4 +- Sources/_StringProcessing/Regex/DSLTree.swift | 6 +-- Tests/RegexBuilderTests/RegexDSLTests.swift | 9 ++++ 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/Sources/RegexBuilder/Variadics.swift b/Sources/RegexBuilder/Variadics.swift index e367a2ecb..c1e380144 100644 --- a/Sources/RegexBuilder/Variadics.swift +++ b/Sources/RegexBuilder/Variadics.swift @@ -784,7 +784,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == Substring { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -795,7 +795,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == Substring { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -913,7 +913,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?), Component.RegexOutput == (W, C1) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -923,7 +923,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?), Component.RegexOutput == (W, C1) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1039,7 +1039,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?), Component.RegexOutput == (W, C1, C2) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1049,7 +1049,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?), Component.RegexOutput == (W, C1, C2) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1165,7 +1165,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?), Component.RegexOutput == (W, C1, C2, C3) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1175,7 +1175,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?), Component.RegexOutput == (W, C1, C2, C3) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1291,7 +1291,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?), Component.RegexOutput == (W, C1, C2, C3, C4) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1301,7 +1301,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?), Component.RegexOutput == (W, C1, C2, C3, C4) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1417,7 +1417,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?), Component.RegexOutput == (W, C1, C2, C3, C4, C5) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1427,7 +1427,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?), Component.RegexOutput == (W, C1, C2, C3, C4, C5) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1543,7 +1543,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1553,7 +1553,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1669,7 +1669,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1679,7 +1679,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1795,7 +1795,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?, C8?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7, C8) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1805,7 +1805,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?, C8?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7, C8) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -1921,7 +1921,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?, C8?, C9?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7, C8, C9) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -1931,7 +1931,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?, C8?, C9?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7, C8, C9) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } @@ -2047,7 +2047,7 @@ extension Repeat { _ component: Component, count: Int ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?, C8?, C9?, C10?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7, C8, C9, C10) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -2057,7 +2057,7 @@ extension Repeat { count: Int, @RegexComponentBuilder _ component: () -> Component ) where RegexOutput == (Substring, C1?, C2?, C3?, C4?, C5?, C6?, C7?, C8?, C9?, C10?), Component.RegexOutput == (W, C1, C2, C3, C4, C5, C6, C7, C8, C9, C10) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } diff --git a/Sources/VariadicsGenerator/VariadicsGenerator.swift b/Sources/VariadicsGenerator/VariadicsGenerator.swift index 98885f6f2..faab75762 100644 --- a/Sources/VariadicsGenerator/VariadicsGenerator.swift +++ b/Sources/VariadicsGenerator/VariadicsGenerator.swift @@ -505,7 +505,7 @@ struct VariadicsGenerator: ParsableCommand { _ component: Component, count: Int ) \(params.whereClauseForInit) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component)) } @@ -516,7 +516,7 @@ struct VariadicsGenerator: ParsableCommand { count: Int, @\(concatBuilderName) _ component: () -> Component ) \(params.whereClauseForInit) { - assert(count > 0, "Must specify a positive count") + precondition(count >= 0, "Must specify a positive count") let factory = makeFactory() self.init(factory.exactly(count, component())) } diff --git a/Sources/_StringProcessing/Regex/DSLTree.swift b/Sources/_StringProcessing/Regex/DSLTree.swift index bcb1c690f..0714c5d2c 100644 --- a/Sources/_StringProcessing/Regex/DSLTree.swift +++ b/Sources/_StringProcessing/Regex/DSLTree.swift @@ -853,9 +853,9 @@ extension DSLTree.Node { _ node: DSLTree.Node ) -> DSLTree.Node { // TODO: Throw these as errors - assert(range.lowerBound >= 0, "Cannot specify a negative lower bound") - assert(!range.isEmpty, "Cannot specify an empty range") - + precondition(range.lowerBound >= 0, "Cannot specify a negative lower bound") + precondition(!range.isEmpty, "Cannot specify an empty range") + let kind: DSLTree.QuantificationKind = behavior .map { .explicit($0.dslTreeKind) } ?? .default diff --git a/Tests/RegexBuilderTests/RegexDSLTests.swift b/Tests/RegexBuilderTests/RegexDSLTests.swift index 85dd048ba..09f7999f9 100644 --- a/Tests/RegexBuilderTests/RegexDSLTests.swift +++ b/Tests/RegexBuilderTests/RegexDSLTests.swift @@ -518,6 +518,15 @@ class RegexDSLTests: XCTestCase { Repeat(0 ... 0) { "a" } } + try _testDSLCaptures( + ("", ""), + ("a", nil), + ("aa", nil), + matchType: Substring.self, ==) + { + Repeat(count: 0) { "a" } + } + try _testDSLCaptures( ("", ""), ("a", "a"),