Skip to content

Commit eb8d41b

Browse files
committed
Update UTF8Span proposal
1 parent 47118c3 commit eb8d41b

File tree

1 file changed

+63
-33
lines changed

1 file changed

+63
-33
lines changed

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

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

@@ -218,7 +227,7 @@ We propose a `UTF8Span.UnicodeScalarIterator` type that can do scalar processing
218227

219228
```swift
220229
extension UTF8Span {
221-
/// Returns an iterator that will decode the code units into
230+
/// Returns an iterator that will decode the code units into
222231
/// `Unicode.Scalar`s.
223232
///
224233
/// The resulting iterator has the same lifetime constraints as `self`.
@@ -316,7 +325,7 @@ extension UTF8Span {
316325

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

319-
The `CharacterIterator` assumes that the start and end of the `UTF8Span` is the start and end of content.
328+
The `CharacterIterator` assumes that the start and end of the `UTF8Span` is the start and end of content.
320329

321330
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).
322331

@@ -343,15 +352,15 @@ extension UTF8Span {
343352
/// Return the `Character` starting at `currentCodeUnitOffset`. After the
344353
/// function returns, `currentCodeUnitOffset` holds the position at the
345354
/// end of the `Character`, which is also the start of the next
346-
/// `Character`.
355+
/// `Character`.
347356
///
348357
/// Returns `nil` if at the end of the `UTF8Span`.
349358
public mutating func next() -> Character?
350359

351360
/// Return the `Character` ending at `currentCodeUnitOffset`. After the
352361
/// function returns, `currentCodeUnitOffset` holds the position at the
353362
/// start of the returned `Character`, which is also the end of the
354-
/// previous `Character`.
363+
/// previous `Character`.
355364
///
356365
/// Returns `nil` if at the start of the `UTF8Span`.
357366
public mutating func previous() -> Character?
@@ -395,7 +404,7 @@ extension UTF8Span {
395404
///
396405
/// Note: This is only for very specific, low-level use cases. If
397406
/// `codeUnitOffset` is not properly scalar-aligned, this function can
398-
/// result in undefined behavior when, e.g., `next()` is called.
407+
/// result in undefined behavior when, e.g., `next()` is called.
399408
///
400409
/// If `i` is scalar-aligned, but not `Character`-aligned, you may get
401410
/// different results from running `Character` iteration.
@@ -445,13 +454,6 @@ extension UTF8Span {
445454
}
446455
```
447456

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

456458
#### Canonical equivalence and ordering
457459

@@ -467,7 +469,7 @@ extension UTF8Span {
467469

468470
/// Whether `self` orders less than `other` under Unicode Canonical
469471
/// Equivalence using normalized code-unit order (in NFC).
470-
public func isCanonicallyLessThan(
472+
public func canonicallyPrecedes(
471473
_ other: UTF8Span
472474
) -> Bool
473475
}
@@ -483,17 +485,17 @@ Slicing a `UTF8Span` is nuanced and depends on the caller's desired use. They ca
483485

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

@@ -621,16 +623,24 @@ extension UTF8Span {
621623
```
622624

623625

624-
625626
### More alignments and alignment queries
626627

627628
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.
628629

629630
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).
630631

632+
### `~=` and other operators
633+
634+
`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.
635+
636+
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).
637+
638+
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.
639+
640+
631641
### Creating `String` copies
632642

633-
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.
643+
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.
634644

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

@@ -640,7 +650,7 @@ Future API could include checks for whether the content is in a particular norma
640650

641651
### UnicodeScalarView and CharacterView
642652

643-
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.
653+
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.
644654

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

@@ -695,6 +705,26 @@ Many printing and logging protocols and facilities operate in terms of `String`.
695705

696706
## Alternatives considered
697707

708+
### Problems arising from the unsafe init
709+
710+
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.
711+
712+
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.
713+
714+
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.
715+
716+
With these two API, you can get to UB via a:
717+
718+
```swift
719+
let codeUnits = unsafe UTF8Span(unsafeAssumingValidUTF8: bytes)
720+
...
721+
String(copying: codeUnits)
722+
```
723+
724+
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).
725+
726+
727+
698728
### Invalid start / end of input UTF-8 encoding errors
699729

700730
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`.
@@ -765,7 +795,7 @@ Scalar-alignment can still be checked and managed by the caller through the `res
765795

766796
#### View Collections
767797

768-
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.
798+
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.
769799

770800
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:
771801

0 commit comments

Comments
 (0)