Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions stdlib/public/core/StringLegacy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,34 @@ extension String {
public func hasPrefix(_ prefix: String) -> Bool {
if _fastPath(self._guts.isNFCFastUTF8 && prefix._guts.isNFCFastUTF8) {
guard prefix._guts.count <= self._guts.count else { return false }
return prefix._guts.withFastUTF8 { nfcPrefix in
let isPrefix = prefix._guts.withFastUTF8 { nfcPrefix in
let prefixEnd = nfcPrefix.count
return self._guts.withFastUTF8(range: 0..<prefixEnd) { nfcSlicedSelf in
return _binaryCompare(nfcSlicedSelf, nfcPrefix) == 0
}
}
let endIndex = Index(_encodedOffset: prefix._guts.count)
// In addition to a byte comparison check, we also need to check that
// the prefix ends on a grapheme cluster boundary of the String
return isPrefix && self._guts.isOnGraphemeClusterBoundary(endIndex)
}

return starts(with: prefix)
}

public func hasSuffix(_ suffix: String) -> Bool {
if _fastPath(self._guts.isNFCFastUTF8 && suffix._guts.isNFCFastUTF8) {
guard suffix._guts.count <= self._guts.count else { return false }
return suffix._guts.withFastUTF8 { nfcSuffix in
let suffixStart = self._guts.count - nfcSuffix.count
let suffixStart = self._guts.count - suffix._guts.count
guard suffixStart >= 0 else { return false }
let isSuffix = suffix._guts.withFastUTF8 { nfcSuffix in
return self._guts.withFastUTF8(range: suffixStart..<self._guts.count) {
nfcSlicedSelf in return _binaryCompare(nfcSlicedSelf, nfcSuffix) == 0
}
}
let startIndex = Index(_encodedOffset: suffixStart)
// In addition to a byte comparison check, we also need to check that
// the suffix starts on a grapheme cluster boundary of the String
return isSuffix && self._guts.isOnGraphemeClusterBoundary(startIndex)
}

return self.reversed().starts(with: suffix.reversed())
Expand Down
28 changes: 12 additions & 16 deletions test/stdlib/StringAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,22 +293,7 @@ StringTests.test("LosslessStringConvertible") {
checkLosslessStringConvertible(comparisonTests.map { $0.rhs })
}

// Mark the test cases that are expected to fail in checkHasPrefixHasSuffix

let substringTests = tests.map {
(test: ComparisonTest) -> ComparisonTest in
switch (test.expectedUnicodeCollation, test.lhs, test.rhs) {

case (.gt, "\r\n", "\n"):
return test.replacingPredicate(.objCRuntime(
"blocked on rdar://problem/19036555"))

default:
return test
}
}

for test in substringTests {
for test in tests {
StringTests.test("hasPrefix,hasSuffix: line \(test.loc.line)")
.skip(.nativeRuntime(
"String.has{Prefix,Suffix} defined when _runtime(_ObjC)"))
Expand Down Expand Up @@ -532,4 +517,15 @@ StringTests.test("_isIdentical(to:)") {
expectTrue(g._isIdentical(to: g))
}

StringTests.test("hasPrefix/hasSuffix vs Character boundaries") {
// https://github.com/apple/swift/issues/67427
let s1 = "\r\n"
let s2 = "\r\n" + "cafe" + "\r\n"

expectFalse(s1.hasPrefix("\r"))
expectFalse(s1.hasSuffix("\n"))
expectFalse(s2.hasPrefix("\r"))
expectFalse(s2.hasSuffix("\n"))
}

runAllTests()