Skip to content

Commit 1da5816

Browse files
authored
Merge pull request #41599 from lorentey/string-unicodescalarview-index-validation
[stdlib] String.UnicodeScalarView: Fix some index validation issues
2 parents 20c293e + 2757088 commit 1da5816

File tree

2 files changed

+73
-4
lines changed

2 files changed

+73
-4
lines changed

stdlib/public/core/StringUnicodeScalarView.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,11 @@ extension String.UnicodeScalarView: BidirectionalCollection {
106106
/// - Precondition: The next location exists.
107107
@inlinable @inline(__always)
108108
public func index(after i: Index) -> Index {
109-
let i = _guts.scalarAlign(i)
110-
_internalInvariant(i < endIndex)
111109
// TODO(String performance): isASCII fast-path
112110

111+
_precondition(i < endIndex, "String index is out of bounds")
112+
let i = _guts.scalarAlign(i)
113+
113114
if _fastPath(_guts.isFastUTF8) {
114115
let len = _guts.fastUTF8ScalarLength(startingAt: i._encodedOffset)
115116
return i.encoded(offsetBy: len)._scalarAligned
@@ -128,10 +129,16 @@ extension String.UnicodeScalarView: BidirectionalCollection {
128129
/// - Precondition: The previous location exists.
129130
@inlinable @inline(__always)
130131
public func index(before i: Index) -> Index {
131-
let i = _guts.scalarAlign(i)
132-
_precondition(i._encodedOffset > 0)
133132
// TODO(String performance): isASCII fast-path
134133

134+
// Note: bounds checking in `index(before:)` is tricky as scalar aligning an
135+
// index may need to access storage, but it may also move it closer towards
136+
// the `startIndex`. Therefore, we must check against the `endIndex` before
137+
// aligning, but we need to delay the `i > startIndex` check until after.
138+
_precondition(i <= endIndex, "String index is out of bounds")
139+
let i = _guts.scalarAlign(i)
140+
_precondition(i > startIndex, "String index is out of bounds")
141+
135142
if _fastPath(_guts.isFastUTF8) {
136143
let len = _guts.withFastUTF8 { utf8 -> Int in
137144
return _utf8ScalarLength(utf8, endingAt: i._encodedOffset)

test/stdlib/StringTraps.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,5 +198,67 @@ StringTraps.test("UTF8ViewIndex/offsetCrash")
198198
_ = s8.utf8[i]
199199
}
200200

201+
StringTraps.test("UnicodeScalarView index(before:) trap on startIndex")
202+
.skip(
203+
.custom({ _isFastAssertConfiguration() },
204+
reason: "trap is not guaranteed to happen in -Ounchecked"))
205+
.code {
206+
guard #available(SwiftStdlib 5.7, *) else { return }
207+
208+
let s = "abc"
209+
var i = s.unicodeScalars.endIndex
210+
i = s.unicodeScalars.index(before: i)
211+
i = s.unicodeScalars.index(before: i)
212+
i = s.unicodeScalars.index(before: i)
213+
expectCrashLater()
214+
i = s.unicodeScalars.index(before: i)
215+
}
216+
217+
StringTraps.test("UnicodeScalarView index(before:) trap on startIndex after scalar alignment")
218+
.skip(
219+
.custom({ _isFastAssertConfiguration() },
220+
reason: "trap is not guaranteed to happen in -Ounchecked"))
221+
.code {
222+
guard #available(SwiftStdlib 5.7, *) else { return }
223+
224+
let s = "🥦 Floret of broccoli"
225+
var i = s.utf8.index(after: s.utf8.startIndex)
226+
expectCrashLater()
227+
// `i` is equivalent to `s.startIndex` as far as `String.UnicodeScalarView` is
228+
// concerned
229+
i = s.unicodeScalars.index(before: i)
230+
}
231+
232+
StringTraps.test("UnicodeScalarView index(after:) trap on endIndex")
233+
.skip(
234+
.custom({ _isFastAssertConfiguration() },
235+
reason: "trap is not guaranteed to happen in -Ounchecked"))
236+
.code {
237+
guard #available(SwiftStdlib 5.7, *) else { return }
238+
239+
let s = "abc"
240+
var i = s.unicodeScalars.startIndex
241+
i = s.unicodeScalars.index(after: i)
242+
i = s.unicodeScalars.index(after: i)
243+
i = s.unicodeScalars.index(after: i)
244+
expectCrashLater()
245+
i = s.unicodeScalars.index(after: i)
246+
}
247+
248+
StringTraps.test("UnicodeScalarView index(after:) trap on i > endIndex")
249+
.skip(
250+
.custom({ _isFastAssertConfiguration() },
251+
reason: "trap is not guaranteed to happen in -Ounchecked"))
252+
.code {
253+
guard #available(SwiftStdlib 5.7, *) else { return }
254+
255+
let long = "abcd"
256+
var i = long.unicodeScalars.endIndex
257+
258+
let s = "abc"
259+
expectCrashLater()
260+
i = s.unicodeScalars.index(after: i)
261+
}
262+
201263
runAllTests()
202264

0 commit comments

Comments
 (0)