Skip to content

Commit 9f90fb1

Browse files
authored
Merge pull request #26575 from numist/numist/application-denied
Return nil on applying() failure instead of crashing
2 parents 30d50fb + 15904df commit 9f90fb1

File tree

2 files changed

+104
-30
lines changed

2 files changed

+104
-30
lines changed

stdlib/public/core/Diffing.swift

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1616
extension CollectionDifference {
1717
fileprivate func _fastEnumeratedApply(
18-
_ consume: (Change) -> Void
19-
) {
18+
_ consume: (Change) throws -> Void
19+
) rethrows {
2020
let totalRemoves = removals.count
2121
let totalInserts = insertions.count
2222
var enumeratedRemoves = 0
@@ -41,7 +41,7 @@ extension CollectionDifference {
4141
preconditionFailure()
4242
}
4343

44-
consume(change)
44+
try consume(change)
4545

4646
switch change {
4747
case .remove(_, _, _):
@@ -53,6 +53,9 @@ extension CollectionDifference {
5353
}
5454
}
5555

56+
// Error type allows the use of throw to unroll state on application failure
57+
private enum _ApplicationError : Error { case failed }
58+
5659
extension RangeReplaceableCollection {
5760
/// Applies the given difference to this collection.
5861
///
@@ -66,45 +69,54 @@ extension RangeReplaceableCollection {
6669
/// number of changes contained by the parameter.
6770
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
6871
public func applying(_ difference: CollectionDifference<Element>) -> Self? {
69-
var result = Self()
70-
var enumeratedRemoves = 0
71-
var enumeratedInserts = 0
72-
var enumeratedOriginals = 0
73-
var currentIndex = self.startIndex
7472

7573
func append(
7674
into target: inout Self,
7775
contentsOf source: Self,
7876
from index: inout Self.Index, count: Int
79-
) {
77+
) throws {
8078
let start = index
81-
source.formIndex(&index, offsetBy: count)
79+
if !source.formIndex(&index, offsetBy: count, limitedBy: source.endIndex) {
80+
throw _ApplicationError.failed
81+
}
8282
target.append(contentsOf: source[start..<index])
8383
}
8484

85-
difference._fastEnumeratedApply { change in
86-
switch change {
87-
case .remove(offset: let offset, element: _, associatedWith: _):
88-
let origCount = offset - enumeratedOriginals
89-
append(into: &result, contentsOf: self, from: &currentIndex, count: origCount)
90-
enumeratedOriginals += origCount + 1 // Removal consumes an original element
91-
currentIndex = self.index(after: currentIndex)
92-
enumeratedRemoves += 1
93-
case .insert(offset: let offset, element: let element, associatedWith: _):
94-
let origCount = (offset + enumeratedRemoves - enumeratedInserts) - enumeratedOriginals
95-
append(into: &result, contentsOf: self, from: &currentIndex, count: origCount)
96-
result.append(element)
97-
enumeratedOriginals += origCount
98-
enumeratedInserts += 1
85+
var result = Self()
86+
do {
87+
var enumeratedRemoves = 0
88+
var enumeratedInserts = 0
89+
var enumeratedOriginals = 0
90+
var currentIndex = self.startIndex
91+
try difference._fastEnumeratedApply { change in
92+
switch change {
93+
case .remove(offset: let offset, element: _, associatedWith: _):
94+
let origCount = offset - enumeratedOriginals
95+
try append(into: &result, contentsOf: self, from: &currentIndex, count: origCount)
96+
if currentIndex == self.endIndex {
97+
// Removing nonexistent element off the end of the collection
98+
throw _ApplicationError.failed
99+
}
100+
enumeratedOriginals += origCount + 1 // Removal consumes an original element
101+
currentIndex = self.index(after: currentIndex)
102+
enumeratedRemoves += 1
103+
case .insert(offset: let offset, element: let element, associatedWith: _):
104+
let origCount = (offset + enumeratedRemoves - enumeratedInserts) - enumeratedOriginals
105+
try append(into: &result, contentsOf: self, from: &currentIndex, count: origCount)
106+
result.append(element)
107+
enumeratedOriginals += origCount
108+
enumeratedInserts += 1
109+
}
110+
_internalInvariant(enumeratedOriginals <= self.count)
111+
}
112+
if currentIndex < self.endIndex {
113+
result.append(contentsOf: self[currentIndex...])
99114
}
100-
_internalInvariant(enumeratedOriginals <= self.count)
115+
_internalInvariant(result.count == self.count + enumeratedInserts - enumeratedRemoves)
116+
} catch {
117+
return nil
101118
}
102-
let origCount = self.count - enumeratedOriginals
103-
append(into: &result, contentsOf: self, from: &currentIndex, count: origCount)
104119

105-
_internalInvariant(currentIndex == self.endIndex)
106-
_internalInvariant(enumeratedOriginals + origCount == self.count)
107-
_internalInvariant(result.count == self.count + enumeratedInserts - enumeratedRemoves)
108120
return result
109121
}
110122
}

test/stdlib/Diffing.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,68 @@ if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) {
629629
}}}}}}
630630
}
631631

632+
suite.test("Fast applicator error condition") {
633+
let bear = "bear"
634+
let bird = "bird"
635+
let bat = "bat"
636+
637+
let diff = bird.difference(from: bear)
638+
639+
expectEqual(nil, bat.applying(diff))
640+
}
641+
642+
suite.test("Fast applicator boundary condition remove last element") {
643+
let base = [1, 2, 3]
644+
645+
expectEqual([1, 2], base.applying(CollectionDifference<Int>([.remove(offset: base.count - 1, element: 3, associatedWith: nil)])!))
646+
}
647+
648+
suite.test("Fast applicator boundary condition append element") {
649+
let base = [1, 2, 3]
650+
651+
expectEqual([1, 2, 3, 4], base.applying(CollectionDifference<Int>([.insert(offset: base.count, element: 4, associatedWith: nil)])!))
652+
}
653+
654+
suite.test("Fast applicator error boundary condition remove at endIndex") {
655+
let base = [1, 2, 3]
656+
657+
expectEqual(nil, base.applying(CollectionDifference<Int>([.remove(offset: base.count, element: 4, associatedWith: nil)])!))
658+
}
659+
660+
suite.test("Fast applicator error boundary condition insert beyond end") {
661+
let base = [1, 2, 3]
662+
663+
expectEqual(nil, base.applying(CollectionDifference<Int>([.insert(offset: base.count + 1, element: 5, associatedWith: nil)])!))
664+
}
665+
666+
suite.test("Fast applicator boundary condition replace tail") {
667+
let base = [1, 2, 3]
668+
669+
expectEqual([1, 2, 4], base.applying(CollectionDifference<Int>([
670+
.remove(offset: base.count - 1, element: 3, associatedWith: nil),
671+
.insert(offset: base.count - 1, element: 4, associatedWith: nil)
672+
])!))
673+
}
674+
675+
suite.test("Fast applicator error boundary condition insert beyond end after tail removal") {
676+
let base = [1, 2, 3]
677+
678+
expectEqual(nil, base.applying(CollectionDifference<Int>([
679+
.remove(offset: base.count - 1, element: 3, associatedWith: nil),
680+
.insert(offset: base.count, element: 4, associatedWith: nil)
681+
])!))
682+
683+
}
684+
685+
suite.test("Fast applicator error boundary condition insert beyond end after non-tail removal") {
686+
let base = [1, 2, 3]
687+
688+
expectEqual(nil, base.applying(CollectionDifference<Int>([
689+
.remove(offset: base.count - 2, element: 2, associatedWith: nil),
690+
.insert(offset: base.count, element: 4, associatedWith: nil)
691+
])!))
692+
}
693+
632694
suite.test("Fast applicator fuzzer") {
633695
func makeArray() -> [Int] {
634696
var arr = [Int]()

0 commit comments

Comments
 (0)