Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create a packed data structure in Swift?

Tags:

swift

I'm converting a project from Objective-C to Swift, and I'm using a packed struct to type cast binary messages sent over a socket:

typedef struct {
    uint16_t version;  // Message format version, currently 0x0100.
    uint32_t length;   // Length of data in bytes.
    uint16_t reserved; // Reserved for future use.
    uint8_t data[];    // Binary encoded plist.
} __attribute__((packed)) mma_msg_t;

I'm not sure what the best approach is in Swift, and the closest approximation I can get is:

struct mma_msg {
    var version: CUnsignedShort     // Message format version, currently 0x0100.
    var length: CUnsignedInt        // Length of data in bytes.
    var reserved: CUnsignedShort    // Reserved for future use.
    var data: CUnsignedChar[]       // Binary encoded plist.
}

Two important details are lost in the translation: there's no guaranteed bitsize of the integer types, and there's no structure packing. I don't think this can be expressed in Swift, but if so, how?

I'm open to suggestions of an alternative approach, e.g. something along the lines of Python's struct module.

like image 692
MagerValp Avatar asked Jun 10 '14 10:06

MagerValp


3 Answers

I started writing a Swift class modeled after Python's struct module, it can be found on github as MVPCStruct.

Code for the first prototype was as follows:

enum Endianness {
    case littleEndian
    case bigEndian
}

// Split a large integer into bytes.
extension Int {
    func splitBytes(endianness: Endianness, size: Int) -> UInt8[] {
        var bytes = UInt8[]()
        var shift: Int
        var step: Int
        if endianness == .littleEndian {
            shift = 0
            step = 8
        } else {
            shift = (size - 1) * 8
            step = -8
        }
        for count in 0..size {
            bytes.append(UInt8((self >> shift) & 0xff))
            shift += step
        }
        return bytes
    }
}
extension UInt {
    func splitBytes(endianness: Endianness, size: Int) -> UInt8[] {
        var bytes = UInt8[]()
        var shift: Int
        var step: Int
        if endianness == .littleEndian {
            shift = 0
            step = 8
        } else {
            shift = Int((size - 1) * 8)
            step = -8
        }
        for count in 0..size {
            bytes.append(UInt8((self >> UInt(shift)) & 0xff))
            shift = shift + step
        }
        return bytes
    }
}


class Struct: NSObject {

    //class let PAD_BYTE = UInt8(0x00)    // error: class variables not yet supported
    //class let ERROR_PACKING = -1

    class func platformEndianness() -> Endianness {
        return .littleEndian
    }

    // Pack an array of data according to the format string. Return NSData
    // or nil if there's an error.
    class func pack(format: String, data: AnyObject[], error: NSErrorPointer) -> NSData? {
        let PAD_BYTE = UInt8(0x00)
        let ERROR_PACKING = -1

        var bytes = UInt8[]()
        var index = 0
        var repeat = 0
        var alignment = true
        var endianness = Struct.platformEndianness()

        // Set error message and return nil.
        func failure(message: String) -> NSData? {
            if error {
                error.memory = NSError(domain: "se.gu.it.GUStructPacker",
                    code: ERROR_PACKING,
                    userInfo: [NSLocalizedDescriptionKey: message])
            }
            return nil
        }

        // If alignment is requested, emit pad bytes until alignment is
        // satisfied.
        func padAlignment(size: Int) {
            if alignment {
                let mask = size - 1
                while (bytes.count & mask) != 0 {
                    bytes.append(PAD_BYTE)
                }
            }
        }

        for c in format {
            // Integers are repeat counters. Consume and continue.
            if let value = String(c).toInt() {
                repeat = repeat * 10 + value
                continue
            }
            // Process repeat count values, minimum of 1.
            for i in 0..(repeat > 0 ? repeat : 1) {
                switch c {

                case "@":
                    endianness = Struct.platformEndianness()
                    alignment = true
                case "=":
                    endianness = Struct.platformEndianness()
                    alignment = false
                case "<":
                    endianness = Endianness.littleEndian
                    alignment = false
                case ">":
                    endianness = Endianness.bigEndian
                    alignment = false
                case "!":
                    endianness = Endianness.bigEndian
                    alignment = false

                case "x":
                    bytes.append(PAD_BYTE)

                default:

                    if index >= data.count {
                        return failure("expected at least \(index) items for packing, got \(data.count)")
                    }
                    let rawValue: AnyObject = data[index++]

                    switch c {

                    case "c":
                        if let str = rawValue as? String {
                            let codePoint = str.utf16[0]
                            if codePoint < 128 {
                                bytes.append(UInt8(codePoint))
                            } else {
                                return failure("char format requires String of length 1")
                            }
                        } else {
                            return failure("char format requires String of length 1")
                        }

                    case "b":
                        if let value = rawValue as? Int {
                            if value >= -0x80 && value <= 0x7f {
                                bytes.append(UInt8(value & 0xff))
                            } else {
                                return failure("value outside valid range of Int8")
                            }
                        } else {
                            return failure("cannot convert argument to Int")
                        }

                    case "B":
                        if let value = rawValue as? UInt {
                            if value > 0xff {
                                return failure("value outside valid range of UInt8")
                            } else {
                                bytes.append(UInt8(value))
                            }
                        } else {
                            return failure("cannot convert argument to UInt")
                        }

                    case "?":
                        if let value = rawValue as? Bool {
                            if value {
                                bytes.append(UInt8(1))
                            } else {
                                bytes.append(UInt8(0))
                            }
                        } else {
                            return failure("cannot convert argument to Bool")
                        }

                    case "h":
                        if let value = rawValue as? Int {
                            if value >= -0x8000 && value <= 0x7fff {
                                padAlignment(2)
                                bytes.extend(value.splitBytes(endianness, size: 2))
                            } else {
                                return failure("value outside valid range of Int16")
                            }
                        } else {
                            return failure("cannot convert argument to Int")
                        }

                    case "H":
                        if let value = rawValue as? UInt {
                            if value > 0xffff {
                                return failure("value outside valid range of UInt16")
                            } else {
                                padAlignment(2)
                                bytes.extend(value.splitBytes(endianness, size: 2))
                            }
                        } else {
                            return failure("cannot convert argument to UInt")
                        }

                    case "i", "l":
                        if let value = rawValue as? Int {
                            if value >= -0x80000000 && value <= 0x7fffffff {
                                padAlignment(4)
                                bytes.extend(value.splitBytes(endianness, size: 4))
                            } else {
                                return failure("value outside valid range of Int32")
                            }
                        } else {
                            return failure("cannot convert argument to Int")
                        }

                    case "I", "L":
                        if let value = rawValue as? UInt {
                            if value > 0xffffffff {
                                return failure("value outside valid range of UInt32")
                            } else {
                                padAlignment(4)
                                bytes.extend(value.splitBytes(endianness, size: 4))
                            }
                        } else {
                            return failure("cannot convert argument to UInt")
                        }

                    case "q":
                        if let value = rawValue as? Int {
                            padAlignment(8)
                            bytes.extend(value.splitBytes(endianness, size: 8))
                        } else {
                            return failure("cannot convert argument to Int")
                        }

                    case "Q":
                        if let value = rawValue as? UInt {
                            padAlignment(8)
                            bytes.extend(value.splitBytes(endianness, size: 8))
                        } else {
                            return failure("cannot convert argument to UInt")
                        }

                    case "f", "d":
                        assert(false, "float/double unimplemented")

                    case "s", "p":
                        assert(false, "cstring/pstring unimplemented")

                    case "P":
                        assert(false, "pointer unimplemented")

                    default:
                        return failure("bad character in format")
                    }
                }
            }
            // Reset the repeat counter.
            repeat = 0
        }

        if index != data.count {
            return failure("expected \(index) items for packing, got \(data.count)")
        }
        return NSData(bytes: bytes, length: bytes.count)
    }

}
like image 88
MagerValp Avatar answered Oct 08 '22 16:10

MagerValp


I decided to try this out since it seemed fun so I came up with a solution for struct.pack(...).

You can also enforce bit-size on integers in your struct:

struct MyMessage
{
    var version : UInt16
    var length : UInt32
    var reserved : UInt16
    var data : UInt8[]
}

Now on to the packed structures...

Integer Extensions

    extension Int
    {
        func loByte() -> UInt8 { return UInt8(self & 0xFF) }
        func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) }
        func loWord() -> Int16 { return Int16(self & 0xFFFF) }
        func hiWord() -> Int16 { return Int16((self >> 16) & 0xFFFF) }
    }

    extension Int16
    {
        func loByte() -> UInt8 { return UInt8(self & 0xFF) }
        func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) }
    }

    extension UInt
    {
        func loByte() -> UInt8 { return UInt8(self & 0xFF) }
        func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) }
        func loWord() -> UInt16 { return UInt16(self & 0xFFFF) }
        func hiWord() -> UInt16 { return UInt16((self >> 16) & 0xFFFF) }
    }

    extension UInt16
    {
        func loByte() -> UInt8 { return UInt8(self & 0xFF) }
        func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) }
    }

Data Packer

class DataPacker
{
    class func pack(format: String, values: AnyObject...) -> String?
    {
        var bytes = UInt8[]()
        var index = 0

        for char in format
        {
            let value : AnyObject! = values[index++]

            switch(char)
            {
                case "h":
                    bytes.append((value as Int).loByte())
                    bytes.append((value as Int).hiByte())
                case "H":
                    bytes.append((value as UInt).loByte())
                    bytes.append((value as UInt).hiByte())
                case "i":
                    bytes.append((value as Int).loWord().loByte())
                    bytes.append((value as Int).loWord().hiByte())
                    bytes.append((value as Int).hiWord().loByte())
                    bytes.append((value as Int).hiWord().hiByte())
                case "I":
                    bytes.append((value as UInt).loWord().loByte())
                    bytes.append((value as UInt).loWord().hiByte())
                    bytes.append((value as UInt).hiWord().loByte())
                    bytes.append((value as UInt).hiWord().hiByte())
                default:
                    println("Unrecognized character: \(char)")
            }
        }

        return String.stringWithBytes(bytes, length: bytes.count, encoding: NSASCIIStringEncoding)
    }
}

Try It Out

let packedString = DataPacker.pack("HHI", values: 0x100, 0x0, 512)
println(packedString)

Notes

This example is extremely simple and has no real error or type checking. Also, it does not really enforce any system byte-order (endianness) so that may be problematic. Hopefully this is somewhat a starting point for those interested.

For unpacking I did notice that Swift allowed returning a variable-sized tuple. For example: func unpack(format: String) -> (AnyObject...) did not give a compile warning. However, I have no idea how you'd return something like that.

like image 7
Erik Avatar answered Oct 08 '22 16:10

Erik


https://github.com/nst/BinUtils/

Akin to Python's struct.pack()

let d = pack("<h2I3sf", [1, 2, 3, "asd", 0.5])
assert(d == unhexlify("0100 02000000 03000000 617364 0000003f"))

Akin to Python's struct.unpack()

let a = unpack(">hBsf", unhexlify("0500 01 41 3fc00000")!)
assert(a[0] as? Int == 1280)
assert(a[1] as? Int == 1)
assert(a[2] as? String == "A")
assert(a[3] as? Double == 1.5)
like image 2
nst Avatar answered Oct 08 '22 15:10

nst