For decoding binary data (in my case, delivered by a Bluetooth device), I've written this struct:
public struct ConsumableByteArray {
private let bytes: [UInt8]
private var idx = 0
enum Error: Swift.Error {
case notEnoughBytes
}
init(data: Data) {
bytes = [UInt8](data)
}
init(bytes: [UInt8]) {
self.bytes = bytes
}
mutating func consume() throws -> UInt8 {
guard idx < bytes.count else {
throw Error.notEnoughBytes
}
defer { idx += 1}
return bytes[idx]
}
mutating func consume() throws -> UInt16 {
guard idx+1 < bytes.count else {
throw Error.notEnoughBytes
}
defer { idx += 2 }
return UInt16(bytes[idx+1]) << 8 + UInt16(bytes[idx])
}
mutating func consume() throws -> Int16 {
guard idx+1 < bytes.count else {
throw Error.notEnoughBytes
}
defer { idx += 2 }
return Int16(bytes[idx+1]) << 8 + Int16(bytes[idx])
}
mutating func consume() throws -> UInt32 {
guard idx+3 < bytes.count else {
throw Error.notEnoughBytes
}
defer { idx += 4 }
// Swift compiler insists on splitting this expression up
let b3 = UInt32(bytes[idx+3]) << 24
let b2 = UInt32(bytes[idx+2]) << 16
let b1 = UInt32(bytes[idx+1]) << 8
let b0 = UInt32(bytes[idx+0]) << 0
return b3 + b2 + b1 + b0
}
}
Given some data buffer, likely containing int
s of varying widths packed together, it allows those fields to be read out:
let buffer = ConsumableByteArray(data: someData)
let header:UInt8 = try buffer.consume()
let word1:UInt16 = try buffer.consume()
let word2:UInt16 = try buffer.consume()
let crc32:UInt32 = try buffer.consume()
Values in the early data may alter the structure of the later data (e.g. whether a feature is supported or not), hence the need for the flexibility to extract data progressively.
Would you write the implementation any differently, or change the API?