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?