Skip to content

Commit 92fe2d7

Browse files
authored
Merge pull request #2753 from milseman/utf8span
Update UTF8Span proposal
2 parents d949b8a + 9232e2f commit 92fe2d7

File tree

1 file changed

+63
-33
lines changed

1 file changed

+63
-33
lines changed

proposals/0464-utf8span-safe-utf8-processing.md

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,6 @@ extension Unicode.UTF8 {
121121
errors (including overlong encodings, surrogates, and invalid code
122122
points), it will produce an error per byte.
123123
124-
Since overlong encodings, surrogates, and invalid code points are erroneous
125-
by the second byte (at the latest), the above definition produces the same
126-
ranges as defining such a sequence as a truncated scalar error followed by
127-
unexpected continuation byte errors. The more semantically-rich
128-
classification is reported.
129-
130-
For example, a surrogate count point sequence `ED A0 80` will be reported
131-
as three `.surrogateCodePointByte` errors rather than a `.truncatedScalar`
132-
followed by two `.unexpectedContinuationByte` errors.
133-
134124
Other commonly reported error ranges can be constructed from this result.
135125
For example, PEP 383's error-per-byte can be constructed by mapping over
136126
the reported range. Similarly, constructing a single error for the longest
@@ -208,6 +198,25 @@ extension UTF8Span {
208198
///
209199
/// The resulting UTF8Span has the same lifetime constraints as `codeUnits`.
210200
public init(validating codeUnits: Span<UInt8>) throws(UTF8.EncodingError)
201+
202+
/// Creates a UTF8Span unsafely containing `uncheckedBytes`, skipping validation.
203+
///
204+
/// `uncheckedBytes` _must_ be valid UTF-8 or else undefined behavior may
205+
/// emerge from any use of the resulting UTF8Span, including any use of a
206+
/// `String` created by copying the resultant UTF8Span
207+
@unsafe
208+
public init(unsafeAssumingValidUTF8 uncheckedCodeUnits: Span<UInt8>)
209+
}
210+
```
211+
212+
Similarly, `String`s can be created from `UTF8Span`s without re-validating their contents.
213+
214+
```swift
215+
extension String {
216+
/// Create's a String containing a copy of the UTF-8 content in `codeUnits`.
217+
/// Skips
218+
/// validation.
219+
public init(copying codeUnits: UTF8Span)
211220
}
212221
```
213222

@@ -217,7 +226,7 @@ We propose a `UTF8Span.UnicodeScalarIterator` type that can do scalar processing
217226

218227
```swift
219228
extension UTF8Span {
220-
/// Returns an iterator that will decode the code units into
229+
/// Returns an iterator that will decode the code units into
221230
/// `Unicode.Scalar`s.
222231
///
223232
/// The resulting iterator has the same lifetime constraints as `self`.
@@ -315,7 +324,7 @@ extension UTF8Span {
315324

316325
We similarly propose a `UTF8Span.CharacterIterator` type that can do grapheme-breaking forwards and backwards.
317326

318-
The `CharacterIterator` assumes that the start and end of the `UTF8Span` is the start and end of content.
327+
The `CharacterIterator` assumes that the start and end of the `UTF8Span` is the start and end of content.
319328

320329
Any scalar-aligned position is a valid place to start or reset the grapheme-breaking algorithm to, though you could get different `Character` output if resetting to a position that isn't `Character`-aligned relative to the start of the `UTF8Span` (e.g. in the middle of a series of regional indicators).
321330

@@ -342,15 +351,15 @@ extension UTF8Span {
342351
/// Return the `Character` starting at `currentCodeUnitOffset`. After the
343352
/// function returns, `currentCodeUnitOffset` holds the position at the
344353
/// end of the `Character`, which is also the start of the next
345-
/// `Character`.
354+
/// `Character`.
346355
///
347356
/// Returns `nil` if at the end of the `UTF8Span`.
348357
public mutating func next() -> Character?
349358

350359
/// Return the `Character` ending at `currentCodeUnitOffset`. After the
351360
/// function returns, `currentCodeUnitOffset` holds the position at the
352361
/// start of the returned `Character`, which is also the end of the
353-
/// previous `Character`.
362+
/// previous `Character`.
354363
///
355364
/// Returns `nil` if at the start of the `UTF8Span`.
356365
public mutating func previous() -> Character?
@@ -394,7 +403,7 @@ extension UTF8Span {
394403
///
395404
/// Note: This is only for very specific, low-level use cases. If
396405
/// `codeUnitOffset` is not properly scalar-aligned, this function can
397-
/// result in undefined behavior when, e.g., `next()` is called.
406+
/// result in undefined behavior when, e.g., `next()` is called.
398407
///
399408
/// If `i` is scalar-aligned, but not `Character`-aligned, you may get
400409
/// different results from running `Character` iteration.
@@ -444,13 +453,6 @@ extension UTF8Span {
444453
}
445454
```
446455

447-
We also support literal (i.e. non-canonical) pattern matching against `StaticString`.
448-
449-
```swift
450-
extension UTF8Span {
451-
static func ~=(_ lhs: UTF8Span, _ rhs: StaticString) -> Bool
452-
}
453-
```
454456

455457
#### Canonical equivalence and ordering
456458

@@ -466,7 +468,7 @@ extension UTF8Span {
466468

467469
/// Whether `self` orders less than `other` under Unicode Canonical
468470
/// Equivalence using normalized code-unit order (in NFC).
469-
public func isCanonicallyLessThan(
471+
public func canonicallyPrecedes(
470472
_ other: UTF8Span
471473
) -> Bool
472474
}
@@ -482,17 +484,17 @@ Slicing a `UTF8Span` is nuanced and depends on the caller's desired use. They ca
482484

483485
```swift
484486
extension UTF8Span {
485-
/// Returns whether contents are known to be all-ASCII. A return value of
486-
/// `true` means that all code units are ASCII. A return value of `false`
487+
/// Returns whether contents are known to be all-ASCII. A return value of
488+
/// `true` means that all code units are ASCII. A return value of `false`
487489
/// means there _may_ be non-ASCII content.
488490
///
489491
/// ASCII-ness is checked and remembered during UTF-8 validation, so this
490-
/// is often equivalent to is-ASCII, but there are some situations where
492+
/// is often equivalent to is-ASCII, but there are some situations where
491493
/// we might return `false` even when the content happens to be all-ASCII.
492494
///
493-
/// For example, a UTF-8 span generated from a `String` that at some point
494-
/// contained non-ASCII content would report false for `isKnownASCII`, even
495-
/// if that String had subsequent mutation operations that removed any
495+
/// For example, a UTF-8 span generated from a `String` that at some point
496+
/// contained non-ASCII content would report false for `isKnownASCII`, even
497+
/// if that String had subsequent mutation operations that removed any
496498
/// non-ASCII content.
497499
public var isKnownASCII: Bool { get }
498500

@@ -620,16 +622,24 @@ extension UTF8Span {
620622
```
621623

622624

623-
624625
### More alignments and alignment queries
625626

626627
Future API could include word iterators (either [simple](https://www.unicode.org/reports/tr18/#Simple_Word_Boundaries) or [default](https://www.unicode.org/reports/tr18/#Default_Word_Boundaries)), line iterators, etc.
627628

628629
Similarly, we could add API directly to `UTF8Span` for testing whether a given code unit offset is suitably aligned (including scalar or grapheme-cluster alignment checks).
629630

631+
### `~=` and other operators
632+
633+
`UTF8Span` supports both binary equivalence and Unicode canonical equivalence. For example, a textual format parser using `UTF8Span` might operate in terms of binary equivalence for processing the textual format itself and then in terms of Unicode canonical equivalnce when interpreting the content of the fields.
634+
635+
We are deferring making any decision on what a "default" comparison semantics should be as future work, which would include defining a `~=` operator (which would allow one to switch over a `UTF8Span` and match against literals).
636+
637+
It may also be the case that it makes more sense for a library or application to define wrapper types around `UTF8Span` which can define `~=` with their preferred comparison semantics.
638+
639+
630640
### Creating `String` copies
631641

632-
We could add an initializer to `String` that makes an owned copy of a `UTF8Span`'s contents. Such an initializer can skip UTF-8 validation.
642+
We could add an initializer to `String` that makes an owned copy of a `UTF8Span`'s contents. Such an initializer can skip UTF-8 validation.
633643

634644
Alternatively, we could defer adding anything until more of the `Container` protocol story is clear.
635645

@@ -639,7 +649,7 @@ Future API could include checks for whether the content is in a particular norma
639649

640650
### UnicodeScalarView and CharacterView
641651

642-
Like `Span`, we are deferring adding any collection-like types to non-escapable `UTF8Span`. Future work could include adding view types that conform to a new `Container`-like protocol.
652+
Like `Span`, we are deferring adding any collection-like types to non-escapable `UTF8Span`. Future work could include adding view types that conform to a new `Container`-like protocol.
643653

644654
See "Alternatives Considered" below for more rationale on not adding `Collection`-like API in this proposal.
645655

@@ -694,6 +704,26 @@ Many printing and logging protocols and facilities operate in terms of `String`.
694704

695705
## Alternatives considered
696706

707+
### Problems arising from the unsafe init
708+
709+
The combination of the unsafe init on `UTF8Span` and the copying init on `String` creates a new kind of easily-accesible backdoor to `String`'s security and safety, namely the invariant that it holds validly encoded UTF-8 when in native form.
710+
711+
Currently, String is 100% safe outside of crazy custom subclass shenanigans (only on ObjC platforms) or arbitrarily scribbling over memory (which is true of all of Swift). Both are highly visible and require writing many lines of advanced-knowledge code.
712+
713+
Without these two API, it is in theory possible to skip validation and produce a String instance of the [indirect contiguous UTF-8](https://forums.swift.org/t/piercing-the-string-veil/21700) flavor through a custom subclass of NSString. But, it is only available on Obj-C platforms and involves creating a custom subclass of `NSString`, having knowledge of lazy bridging internals (which can and sometimes do change from release to release of Swift), and writing very specialized code. The product would be an unsafe lazily bridged instance of `String`, which could more than offset any performance gains from the workaround itself.
714+
715+
With these two API, you can get to UB via a:
716+
717+
```swift
718+
let codeUnits = unsafe UTF8Span(unsafeAssumingValidUTF8: bytes)
719+
...
720+
String(copying: codeUnits)
721+
```
722+
723+
We are (very) weakly in favor of keeping the unsafe init, because there are many low-level situations in which the valid-UTF8 invariant is held by the system itself (such as a data structure using a custom allocator).
724+
725+
726+
697727
### Invalid start / end of input UTF-8 encoding errors
698728

699729
Earlier prototypes had `.invalidStartOfInput` and `.invalidEndOfInput` UTF8 validation errors to communicate that the input was perhaps incomplete or not slices along scalar boundaries. In this scenario, `.invalidStartOfInput` is equivalent to `.unexpectedContinuation` with the range's lower bound equal to 0 and `.invalidEndOfInput` is equivalent to `.truncatedScalar` with the range's upper bound equal to `count`.
@@ -764,7 +794,7 @@ Scalar-alignment can still be checked and managed by the caller through the `res
764794

765795
#### View Collections
766796

767-
Another forumulation of these operations could be to provide a collection-like API phrased in terms of indices. Because `Collection`s are `Escapable`, we cannot conform nested `View` types to `Collection` so these would not benefit from any `Collection`-generic code, algorithms, etc.
797+
Another forumulation of these operations could be to provide a collection-like API phrased in terms of indices. Because `Collection`s are `Escapable`, we cannot conform nested `View` types to `Collection` so these would not benefit from any `Collection`-generic code, algorithms, etc.
768798

769799
A benefit of such `Collection`-like views is that it could help serve as adapter code for migration. Existing `Collection`-generic algorithms and methods could be converted to support `UTF8Span` via copy-paste-edit. That is, a developer could interact with `UTF8Span` ala:
770800

0 commit comments

Comments
 (0)