Skip to content

Commit 392d900

Browse files
authored
Merge pull request swiftlang#2603 from readdle/nsstring-paragraph-range
NSString._getBlockStart - loose precondition check to avoid crash in edge case
2 parents 83bfa74 + ef75c24 commit 392d900

File tree

2 files changed

+95
-1
lines changed

2 files changed

+95
-1
lines changed

Foundation/NSString.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ extension NSString {
727727
let len = length
728728
var ch: unichar
729729

730-
precondition(range.length <= len && range.location < len - range.length, "Range {\(range.location), \(range.length)} is out of bounds of length \(len)")
730+
precondition(range.length <= len && range.location <= len - range.length, "Range {\(range.location), \(range.length)} is out of bounds of length \(len)")
731731

732732
if range.location == 0 && range.length == len && contentsEndPtr == nil { // This occurs often
733733
startPtr?.pointee = 0

TestFoundation/TestNSString.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,99 @@ class TestNSString: LoopbackServerTest {
14951495
}
14961496
}
14971497

1498+
func test_paragraphRange() {
1499+
let text = "Klaatu\nbarada\r\nnikto.\rRemember 🟨those\u{2029}words."
1500+
let nsText = text as NSString
1501+
1502+
// Expected paragraph ranges in test string
1503+
let paragraphRanges = [
1504+
NSRange(location: 0, length: 7),
1505+
NSRange(location: 7, length: 8),
1506+
NSRange(location: 15, length: 7),
1507+
NSRange(location: 22, length: 17),
1508+
NSRange(location: 39, length: 6),
1509+
]
1510+
1511+
// We also will check ranges across two consecutive paragraphs.
1512+
// Generate pairs from plain array.
1513+
let paragraphPairs = paragraphRanges.enumerated().compactMap { i, range -> (NSRange, NSRange)? in
1514+
guard i < paragraphRanges.count - 1 else {
1515+
return nil
1516+
}
1517+
1518+
return (range, paragraphRanges[i + 1])
1519+
}
1520+
1521+
// Helper function. Generates all possible subranges in provided range.
1522+
// Interrupts if handler returns false.
1523+
func subranges(in range: NSRange, with handler: (NSRange) -> Bool) {
1524+
for location in range.location..<(range.location + range.length) {
1525+
let maxLength = range.length - (location - range.location)
1526+
for length in 0...maxLength {
1527+
let generatedRange = NSRange(location: location, length: length)
1528+
1529+
guard handler(generatedRange) else {
1530+
return
1531+
}
1532+
}
1533+
}
1534+
}
1535+
1536+
// Simplest check. Whole string is one or more
1537+
// paragraphs, so result range should cover it completely.
1538+
let wholeStringRange = NSRange(location: 0, length: nsText.length)
1539+
let allParagrapsRange = nsText.paragraphRange(for: wholeStringRange)
1540+
XCTAssertEqual(wholeStringRange, allParagrapsRange)
1541+
1542+
// Every paragraph is checked against all possible subranges in it.
1543+
for expectedRange in paragraphRanges {
1544+
subranges(in: expectedRange) { generatedRange in
1545+
let calculatedRange = nsText.paragraphRange(for: generatedRange)
1546+
1547+
// One fail report is enough.
1548+
// Otherwise there will be hundreds.
1549+
// Using manual check (not XCTAssertEqual)
1550+
// for early exit.
1551+
guard calculatedRange == expectedRange else {
1552+
XCTFail("paragraphRange(for:) returned \(calculatedRange) for \(generatedRange), but expected is \(expectedRange)")
1553+
return false
1554+
}
1555+
1556+
return true
1557+
}
1558+
}
1559+
1560+
// Every paragraph pair is checked against all possible
1561+
// subranges in single continuous range of both paragraphs.
1562+
for paragraphPair in paragraphPairs {
1563+
let paragraphPairRange = NSRange(location: paragraphPair.0.location, length: paragraphPair.0.length + paragraphPair.1.length)
1564+
subranges(in: paragraphPairRange) { generatedRange in
1565+
let calculatedRange = nsText.paragraphRange(for: generatedRange)
1566+
1567+
let expectedRange: NSRange = {
1568+
// Does it fit in first paragraph range?
1569+
if paragraphPair.0.intersection(generatedRange) == generatedRange {
1570+
return paragraphPair.0
1571+
}
1572+
// Does it fit in second paragraph range?
1573+
if paragraphPair.1.intersection(generatedRange) == generatedRange {
1574+
return paragraphPair.1
1575+
}
1576+
// Neither completely in first, nor in second. Must be partially in both.
1577+
return paragraphPairRange
1578+
}()
1579+
1580+
// Again, manual check with early exit
1581+
guard calculatedRange == expectedRange else {
1582+
XCTFail("paragraphRange(for:) returned \(calculatedRange) for \(generatedRange), but expected \(expectedRange)")
1583+
return false
1584+
}
1585+
1586+
return true
1587+
}
1588+
}
1589+
}
1590+
14981591
static var allTests: [(String, (TestNSString) -> () throws -> Void)] {
14991592
return [
15001593
("test_initData", test_initData),
@@ -1567,6 +1660,7 @@ class TestNSString: LoopbackServerTest {
15671660
("test_lineRangeFor", test_lineRangeFor),
15681661
("test_fileSystemRepresentation", test_fileSystemRepresentation),
15691662
("test_enumerateSubstrings", test_enumerateSubstrings),
1663+
("test_paragraphRange", test_paragraphRange),
15701664
]
15711665
}
15721666
}

0 commit comments

Comments
 (0)