Skip to content

Commit 7374e59

Browse files
authored
Merge pull request #908 from pushkarnk/substring-crash
2 parents 0fd21a9 + bf3c6f2 commit 7374e59

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

Foundation/NSString.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,49 @@ extension NSString {
346346
let start = _storage.utf16.startIndex
347347
let min = start.advanced(by: range.location)
348348
let max = start.advanced(by: range.location + range.length)
349-
return String(_storage.utf16[min..<max])!
349+
if let substr = String(_storage.utf16[min..<max]) {
350+
return substr
351+
}
352+
//If we come here, then the range has created unpaired surrogates on either end.
353+
//An unpaired surrogate is replaced by OXFFFD - the Unicode Replacement Character.
354+
//The CRLF ("\r\n") sequence is also treated like a surrogate pair, but its constinuent
355+
//characters "\r" and "\n" can exist outside the pair!
356+
357+
let replacementCharacter = String(describing: UnicodeScalar(0xFFFD)!)
358+
let CR: UInt16 = 13 //carriage return
359+
let LF: UInt16 = 10 //new line
360+
361+
//make sure the range is of non-zero length
362+
guard range.length > 0 else { return "" }
363+
364+
//if the range is pointing to a single unpaired surrogate
365+
if range.length == 1 {
366+
switch _storage.utf16[min] {
367+
case CR: return "\r"
368+
case LF: return "\n"
369+
default: return replacementCharacter
370+
}
371+
}
372+
373+
//set the prefix and suffix characters
374+
let prefix = _storage.utf16[min] == LF ? "\n" : replacementCharacter
375+
let suffix = _storage.utf16[max.advanced(by: -1)] == CR ? "\r" : replacementCharacter
376+
377+
//if the range breaks a surrogate pair at the beginning of the string
378+
if let substrSuffix = String(_storage.utf16[min.advanced(by: 1)..<max]) {
379+
return prefix + substrSuffix
380+
}
381+
382+
//if the range breaks a surrogate pair at the end of the string
383+
if let substrPrefix = String(_storage.utf16[min..<max.advanced(by: -1)]) {
384+
return substrPrefix + suffix
385+
}
386+
387+
//the range probably breaks surrogate pairs at both the ends
388+
guard min.advanced(by: 1) <= max.advanced(by: -1) else { return prefix + suffix }
389+
390+
let substr = String(_storage.utf16[min.advanced(by: 1)..<max.advanced(by: -1)])!
391+
return prefix + substr + suffix
350392
} else {
351393
let buff = UnsafeMutablePointer<unichar>.allocate(capacity: range.length)
352394
getCharacters(buff, range: range)

TestFoundation/TestNSString.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class TestNSString : XCTestCase {
9595
("test_reflection", { _ in test_reflection }),
9696
("test_replacingOccurrences", test_replacingOccurrences),
9797
("test_getLineStart", test_getLineStart),
98+
("test_substringWithRange", test_substringWithRange),
9899
]
99100
}
100101

@@ -1056,6 +1057,49 @@ class TestNSString : XCTestCase {
10561057
XCTAssertEqual(outContentsEndIndex, twoLines.index(twoLines.startIndex, offsetBy: 11))
10571058
XCTAssertEqual(outEndIndex, twoLines.index(twoLines.startIndex, offsetBy: 12))
10581059
}
1060+
1061+
func test_emptyStringPrefixAndSuffix() {
1062+
let testString = "hello"
1063+
XCTAssertTrue(testString.hasPrefix(""))
1064+
XCTAssertTrue(testString.hasSuffix(""))
1065+
}
1066+
1067+
func test_substringWithRange() {
1068+
let trivial = NSString(string: "swift.org")
1069+
XCTAssertEqual(trivial.substring(with: NSMakeRange(0, 5)), "swift")
1070+
1071+
let surrogatePairSuffix = NSString(string: "Hurray🎉")
1072+
XCTAssertEqual(surrogatePairSuffix.substring(with: NSMakeRange(0, 7)), "Hurray�")
1073+
1074+
let surrogatePairPrefix = NSString(string: "🐱Cat")
1075+
XCTAssertEqual(surrogatePairPrefix.substring(with: NSMakeRange(1, 4)), "�Cat")
1076+
1077+
let singleChar = NSString(string: "😹")
1078+
XCTAssertEqual(singleChar.substring(with: NSMakeRange(0,1)), "")
1079+
1080+
let crlf = NSString(string: "\r\n")
1081+
XCTAssertEqual(crlf.substring(with: NSMakeRange(0,1)), "\r")
1082+
XCTAssertEqual(crlf.substring(with: NSMakeRange(1,1)), "\n")
1083+
XCTAssertEqual(crlf.substring(with: NSMakeRange(1,0)), "")
1084+
1085+
let bothEnds1 = NSString(string: "😺😺")
1086+
XCTAssertEqual(bothEnds1.substring(with: NSMakeRange(1,2)), "��")
1087+
1088+
let s1 = NSString(string: "😺\r\n")
1089+
XCTAssertEqual(s1.substring(with: NSMakeRange(1,2)), "\r")
1090+
1091+
let s2 = NSString(string: "\r\n😺")
1092+
XCTAssertEqual(s2.substring(with: NSMakeRange(1,2)), "\n")
1093+
1094+
let s3 = NSString(string: "😺cats😺")
1095+
XCTAssertEqual(s3.substring(with: NSMakeRange(1,6)), "�cats�")
1096+
1097+
let s4 = NSString(string: "😺cats\r\n")
1098+
XCTAssertEqual(s4.substring(with: NSMakeRange(1,6)), "�cats\r")
1099+
1100+
let s5 = NSString(string: "\r\ncats😺")
1101+
XCTAssertEqual(s5.substring(with: NSMakeRange(1,6)), "\ncats�")
1102+
}
10591103
}
10601104

10611105
struct ComparisonTest {

0 commit comments

Comments
 (0)