Crash During Int to UInt Converting | Swift

Igor Sorokin (srk1nn)
3 min readFeb 17, 2024

--

In this post, I want to discuss a crash, that we encounter in production code. The bug was related to a wrong expectation of API. I believe, everybody can face the same bug, even if the code looks right.

Our problem

In our source code, we needed to save a file on disk. So we had to check that a user had enough space. We use the URL and URLResourceValues for it. And the logic was wrapped into a special provider. The simplified version is listed below.

struct SpaceInfo {
let total: UInt64
let available: UInt64
}

final class AvailableSpaceProvider {

func getSpaceInfo() -> SpaceInfo? {
guard
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last,
let values = try? url.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey]),
let total = values.volumeTotalCapacity,
let available = values.volumeAvailableCapacity
else {
return nil
}

return SpaceInfo(total: UInt64(total), available: UInt64(available))
}
}

When we started getting crash logs, they looked something like that

EXC_BREAKPOINT ...
0 AvailableSpaceProvider.getSpaceInfo()
....

Talked to other developers, we understand that volumeTotalCapacityKey and volumeAvailableCapacityKey may return negative values. It’s unusual, how available and total space can be negative?

The crash was happening during Int to UInt converting. However, I expected that UInt from Int initializer returns something like zero, when Int is negative. Which turned out to be wrong. It’s also describe in the documentation.

Use this initializer to convert from another integer type when you know the value is within the bounds of this type. Passing a value that can’t be represented in this type results in a runtime error.

Solution

From the Swift perspective, you should explicitly specify how you want to convert values. The foundation provides several initializers for that. Let’s discuss it.

init?(exactly:)

Use the init?(exactly:) initializer to create a new instance after checking whether the passed value is representable. Instead of trapping on out-of-range values, using the failable init?(exactly:) initializer results in nil.

let x = Int16(exactly: 500)
// x == Optional(500)

let y = Int8(exactly: 500)
// y == nil

init(clamping:)

Use the init(clamping:) initializer to create a new instance of a binary integer type where out-of-range values are clamped to the representable range of the type. For a type T, the resulting value is in the range T.min...T.max.

let x = Int16(clamping: 500)
// x == 500

let y = Int8(clamping: 500)
// y == 127

let z = UInt8(clamping: -500)
// z == 0

init(truncatingIfNeeded:)

Use the init(truncatingIfNeeded:) initializer to create a new instance with the same bit pattern as the passed value, extending or truncating the value's representation as necessary.

let q: Int16 = 850
// q == 0b00000011_01010010

let r = Int8(truncatingIfNeeded: q) // truncate 'q' to fit in 8 bits
// r == 82
// == 0b01010010

let s = Int16(truncatingIfNeeded: r) // extend 'r' to fill 16 bits
// s == 82
// == 0b00000000_01010010

Note that when negative integers are extended, the result is padded with ones. In my opinion, you’ll rarely need it, but it’s good to know about. Most of the time you’ll use exactly or clamping initializers.

Final thoughts

This information is not only about Int to UInt, but for all convertings for numeric types. If you are absolutely sure, that one type fits into another, use the default initializer (since it does not perform any checks, it is faster). But if you are not sure, use the initializers described above.

dont forget

--

--