hex/binary string conversion in Swift
Asked Answered
G

3

3

Python has two very useful library method (binascii.a2b_hex(keyStr) and binascii.hexlify(keyBytes)) which I have been struggling with in Swift. Is there anything readily available in Swift. If not, how would one implement it? Given all the bounds and other checks (like even-length key) are done.

Glossa answered 27/10, 2016 at 4:47 Comment(1)
@anonymous these answers don't apply on big numbers. I'm talking about 32 digit hex numbers. The efficient way would be to have such a large string and transform the input into a byte array. Swift stdlib doesn't have BigInteger and these kind of conversions in stdlib as of now.Glossa
W
21

Data from Swift 3 has no "built-in" method to print its contents as a hex string, or to create a Data value from a hex string.

"Data to hex string" methods can be found e.g. at How to convert Data to hex string in swift or How can I print the content of a variable of type Data using Swift? or converting String to Data in swift 3.0. Here is an implementation from the first link:

extension Data {
    func hexEncodedString() -> String {
        return map { String(format: "%02hhx", $0) }.joined()
    }
}

Here is a possible implementation of the reverse "hex string to Data" conversion (taken from Hex String to Bytes (NSData) on Code Review, translated to Swift 3 and improved) as a failable inititializer:

extension Data {

    init?(fromHexEncodedString string: String) {

        // Convert 0 ... 9, a ... f, A ...F to their decimal value,
        // return nil for all other input characters
        func decodeNibble(u: UInt8) -> UInt8? {
            switch(u) {
            case 0x30 ... 0x39:
                return u - 0x30
            case 0x41 ... 0x46:
                return u - 0x41 + 10
            case 0x61 ... 0x66:
                return u - 0x61 + 10
            default:
                return nil
            }
        }

        self.init(capacity: string.utf8.count/2)
        
        var iter = string.utf8.makeIterator()
        while let c1 = iter.next() {
            guard
                let val1 = decodeNibble(u: c1),
                let c2 = iter.next(),
                let val2 = decodeNibble(u: c2)
            else { return nil }
            self.append(val1 << 4 + val2)
        }
    }
}

Example:

// Hex string to Data:
if let data = Data(fromHexEncodedString: "0002468A13579BFF") {
    let idata = Data(data.map { 255 - $0 })
    
    // Data to hex string:
    print(idata.hexEncodedString()) // fffdb975eca86400
} else {
    print("invalid hex string")
}
Whitford answered 27/10, 2016 at 7:13 Comment(7)
This deserves 1,000,000 points. Super clean and useful. Thanks for sharing!Dale
Swift Bool type has a mutating method called toggle even.toggle()Benitobenjamen
@LeoDabus: Yes, thanks! (That was introduced in Swift 4.2, after I wrote this answer.) – Anyway, I decided to get rid of the toggle. Also using the UTF-8 representation is more natural now since that is the native representation since Swift 5.Whitford
@MartinR I hope you don't mind I had to edit your post. You should append val1 << 4 + val2 instead of val1 << 8 + val2. Btw Why don't you make your initializer generic as I did? it would allow you to init Data as well as array of bytes. Is there any advantage using the Data capacity initializer instead of calling reserveCapacity after initializing an emptyCollection?Benitobenjamen
@LeoDabus: Oops, thanks, that was a silly error in the last edit. – I don't think there is a difference between the two ways to reserve capacity. But I am not sure anymore if reserving capacity outweighs the costs of determining the string length in advance.Whitford
@MartinR is there any advantage using withContiguousStorageIfAvailable something like string.utf8.withContiguousStorageIfAvailable { var iter = $0.makeIterator()?Benitobenjamen
@LeoDabus: Perhaps, or just string.withCString(...). Perhaps I'll try that sometime. But I like the current implementation because it is relatively short, easy to understand, and reasonably fast in my (few) tests.Whitford
S
1

Not really familiar with Python and the checks it performs when convert the numbers, but you can expand the function below:

func convert(_ str: String, fromRadix r1: Int, toRadix r2: Int) -> String? {
    if let num = Int(str, radix: r1) {
        return String(num, radix: r2)
    } else {
        return nil
    }
}

convert("11111111", fromRadix: 2, toRadix: 16)
convert("ff", fromRadix: 16, toRadix: 2)
Sweeps answered 27/10, 2016 at 5:12 Comment(0)
C
0

Swift 2

extension NSData {
    class func dataFromHexString(hex: String) -> NSData? {
        let regex = try! NSRegularExpression(pattern: "^[0-9a-zA-Z]*$", options: .CaseInsensitive)
        let validate = regex.firstMatchInString(hex, options: NSMatchingOptions.init(rawValue: 0), range: NSRange(location: 0, length: hex.characters.count))
        if validate == nil || hex.characters.count % 2 != 0 {
            return nil
        }
        let data = NSMutableData()
        for i in 0..<hex.characters.count/2 {
            let hexStr = hex.substring(i * 2, length: 2)
            var ch: UInt32 = 0
            NSScanner(string: hexStr).scanHexInt(&ch)
            data.appendBytes(&ch, length: 1)
        }
        return data
    }
}

let a = 0xabcd1234
print(String(format: "%x", a)) // Hex to String
NSData.dataFromHexString("abcd1234") // String to hex
Ceolaceorl answered 27/10, 2016 at 5:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.