Never use the buffer count in String.init(unsafeUninitializedCapacity:initializingUTF8With:)

here’s a weird bug:

var description:String
{
    .init(unsafeUninitializedCapacity: self.rawValue.utf8.count)
    {
        for (i, codeunit):(Int, UInt8) in zip($0.indices, self.rawValue.utf8)
        {
            switch codeunit
            {
            case 0x09, 0x20:    $0[i] = 0x2E // '.'
            case let codeunit:  $0[i] = codeunit
            }
        }
        return $0.count
    }
}

this produces an empty string if the rawValue has less than 16 UTF-8 bytes, because the buffer count ($0.count) is always at least 16.

the fix, as it turns out, is:

@inlinable public
var description:String
{
    .init(unsafeUninitializedCapacity: self.rawValue.utf8.count)
    {
        var i:Int = $0.startIndex
        for codeunit:UInt8 in self.rawValue.utf8
        {
            switch codeunit
            {
            case 0x09, 0x20:    $0[i] = 0x2E // '.'
            case let codeunit:  $0[i] = codeunit
            }

            i = $0.index(after: i)
        }
        return i
    }
}

why on earth does String.init(unsafeUninitializedCapacity:initializingUTF8With:) even pass a buffer with a count that doesn’t match the specified capacity?

1 Like

Small strings have in-line buffer storage with fixed capacity. I think the uninitialized buffer will always have at least the requested capacity, but it may well have more (as is the case for Array.reserveCapacity). I would code defensively: you have easy access to both the capacity requested and the capacity received here.

2 Likes

Incidentally, this is guaranteed to work but only because startIndex is guaranteed to be 0 and index(after:) increments by 1. The return value is supposed to be the count of initialized code units, so it’s a counting exercise semantically independent of the buffer’s indices.

1 Like

I reported this issue (circa Swift 5.5) as apple/swift#57911. The current implementation seems incorrect because _SmallString.capacity is only 10, 14, or 15 bytes.

UPDATE: This issue has been fixed (on the main branch only, for Swift 5.10+).

2 Likes