简体   繁体   中英

Converting Hex String to NSData in Swift

I got the code to convert String to HEX-String in Objective-C:

- (NSString *) CreateDataWithHexString:(NSString*)inputString {
    NSUInteger inLength = [inputString length];    
      
    unichar *inCharacters = alloca(sizeof(unichar) * inLength);
    [inputString getCharacters:inCharacters range:NSMakeRange(0, inLength)];
    
    UInt8 *outBytes = malloc(sizeof(UInt8) * ((inLength / 2) + 1));
    
    NSInteger i, o = 0;
    UInt8 outByte = 0;

    for (i = 0; i < inLength; i++) {
        UInt8 c = inCharacters[i];
        SInt8 value = -1;
        
        if      (c >= '0' && c <= '9') value =      (c - '0');
        else if (c >= 'A' && c <= 'F') value = 10 + (c - 'A');
        else if (c >= 'a' && c <= 'f') value = 10 + (c - 'a');
        
        if (value >= 0) {
            if (i % 2 == 1) {
                outBytes[o++] = (outByte << 4) | value;
                outByte = 0;
            } else {
                outByte = value;
            }
            
        } else {
            if (o != 0) break;
        }
    }
    
    NSData *a = [[NSData alloc] initWithBytesNoCopy:outBytes length:o freeWhenDone:YES];
    NSString* newStr = [NSString stringWithUTF8String:[a bytes]];
    return newStr;
}

I want the same in Swift. Can anybody translate this code in Swift, or is there any easy way to do this in Swift?

This is my hex string to Data routine:

extension String {
    
    /// Create `Data` from hexadecimal string representation
    ///
    /// This creates a `Data` object from hex string. Note, if the string has any spaces or non-hex characters (e.g. starts with '<' and with a '>'), those are ignored and only hex characters are processed.
    ///
    /// - returns: Data represented by this hexadecimal string.
    
    var hexadecimal: Data? {
        var data = Data(capacity: count / 2)
        
        let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
        regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in
            let byteString = (self as NSString).substring(with: match!.range)
            let num = UInt8(byteString, radix: 16)!
            data.append(num)
        }
        
        guard data.count > 0 else { return nil }
        
        return data
    }
    
}

And for the sake of completeness, this is my Data to hex string routine:

extension Data {
    
    /// Hexadecimal string representation of `Data` object.
    
    var hexadecimal: String {
        return map { String(format: "%02x", $0) }
            .joined()
    }
}

Note, as shown in the above, I generally only convert between hexadecimal representations and NSData instances (because if the information could have been represented as a string you probably wouldn't have created a hexadecimal representation in the first place). But your original question wanted to convert between hexadecimal representations and String objects, and that might look like so:

extension String {
    
    /// Create `String` representation of `Data` created from hexadecimal string representation
    ///
    /// This takes a hexadecimal representation and creates a String object from that. Note, if the string has any spaces, those are removed. Also if the string started with a `<` or ended with a `>`, those are removed, too.
    ///
    /// For example,
    ///
    ///     String(hexadecimal: "<666f6f>")
    ///
    /// is
    ///
    ///     Optional("foo")
    ///
    /// - returns: `String` represented by this hexadecimal string.
    
    init?(hexadecimal string: String, encoding: String.Encoding = .utf8) {
        guard let data = string.hexadecimal() else {
            return nil
        }
        
        self.init(data: data, encoding: encoding)
    }
            
    /// Create hexadecimal string representation of `String` object.
    ///
    /// For example,
    ///
    ///     "foo".hexadecimalString()
    ///
    /// is
    ///
    ///     Optional("666f6f")
    ///
    /// - parameter encoding: The `String.Encoding` that indicates how the string should be converted to `Data` before performing the hexadecimal conversion.
    ///
    /// - returns: `String` representation of this String object.
    
    func hexadecimalString(encoding: String.Encoding = .utf8) -> String? {
        return data(using: encoding)?
            .hexadecimal
    }
    
}

You could then use the above like so:

let hexString = "68656c6c 6f2c2077 6f726c64"
print(String(hexadecimal: hexString))

Or,

let originalString = "hello, world"
print(originalString.hexadecimalString())

For permutations of the above for earlier Swift versions, see the revision history of this question.

convert hex string to data and string:

Swift1

func dataWithHexString(hex: String) -> NSData {
    var hex = hex
    let data = NSMutableData()
    while(countElements(hex) > 0) {
        var c: String = hex.substringToIndex(advance(hex.startIndex, 2))
        hex = hex.substringFromIndex(advance(hex.startIndex, 2))
        var ch: UInt32 = 0
        NSScanner(string: c).scanHexInt(&ch)
        data.appendBytes(&ch, length: 1)
    }
    return data
}

use:

let data = dataWithHexString("68656c6c6f2c20776f726c64") // <68656c6c 6f2c2077 6f726c64>
if let string = NSString(data: data, encoding: 1) {
    print(string) // hello, world
}

Swift2

func dataWithHexString(hex: String) -> NSData {
    var hex = hex
    let data = NSMutableData()
    while(hex.characters.count > 0) {
        let c: String = hex.substringToIndex(hex.startIndex.advancedBy(2))
        hex = hex.substringFromIndex(hex.startIndex.advancedBy(2))
        var ch: UInt32 = 0
        NSScanner(string: c).scanHexInt(&ch)
        data.appendBytes(&ch, length: 1)
    }
    return data
}

use:

let data = dataWithHexString("68656c6c6f2c20776f726c64") // <68656c6c 6f2c2077 6f726c64>
if let string = String(data: data, encoding: NSUTF8StringEncoding) {
    print(string) //"hello, world"
}

Swift3

func dataWithHexString(hex: String) -> Data {
    var hex = hex
    var data = Data()
    while(hex.characters.count > 0) {
        let c: String = hex.substring(to: hex.index(hex.startIndex, offsetBy: 2))
        hex = hex.substring(from: hex.index(hex.startIndex, offsetBy: 2))
        var ch: UInt32 = 0
        Scanner(string: c).scanHexInt32(&ch)
        var char = UInt8(ch)
        data.append(&char, count: 1)
    }
    return data
}

use:

let data = dataWithHexString(hex: "68656c6c6f2c20776f726c64") // <68656c6c 6f2c2077 6f726c64>
let string = String(data: data, encoding: .utf8) // "hello, world"

Swift4

func dataWithHexString(hex: String) -> Data {
    var hex = hex
    var data = Data()
    while(hex.count > 0) {
        let subIndex = hex.index(hex.startIndex, offsetBy: 2)
        let c = String(hex[..<subIndex])
        hex = String(hex[subIndex...])
        var ch: UInt32 = 0
        Scanner(string: c).scanHexInt32(&ch)
        var char = UInt8(ch)
        data.append(&char, count: 1)
    }
    return data
}

use:

let data = dataWithHexString(hex: "68656c6c6f2c20776f726c64") // <68656c6c 6f2c2077 6f726c64>
let string = String(data: data, encoding: .utf8) // "hello, world"

Swift 4 & Swift 5 implementation:

init?(hexString: String) {
  let len = hexString.count / 2
  var data = Data(capacity: len)
  var i = hexString.startIndex
  for _ in 0..<len {
    let j = hexString.index(i, offsetBy: 2)
    let bytes = hexString[i..<j]
    if var num = UInt8(bytes, radix: 16) {
      data.append(&num, count: 1)
    } else {
      return nil
    }
    i = j
  }
  self = data
}

Usage:

let data = Data(hexString: "0a1b3c4d")

Here is my Swift 5 way to do it:

  • does take care of "0x" prefixes
  • use subscript instead of allocated Array(), no C style [i+1] too
  • add .hexadecimal to String.data(using encoding:) -> Data?

.

String Extension:

    extension String {
        enum ExtendedEncoding {
            case hexadecimal
        }

        func data(using encoding:ExtendedEncoding) -> Data? {
            let hexStr = self.dropFirst(self.hasPrefix("0x") ? 2 : 0)

            guard hexStr.count % 2 == 0 else { return nil }

            var newData = Data(capacity: hexStr.count/2)

            var indexIsEven = true
            for i in hexStr.indices {
                if indexIsEven {
                    let byteRange = i...hexStr.index(after: i)
                    guard let byte = UInt8(hexStr[byteRange], radix: 16) else { return nil }
                    newData.append(byte)
                }
                indexIsEven.toggle()
            }
            return newData
        }
    }

Usage:

    "5413".data(using: .hexadecimal)
    "0x1234FF".data(using: .hexadecimal)

Tests:

    extension Data {
        var bytes:[UInt8] { // fancy pretty call: myData.bytes -> [UInt8]
            return [UInt8](self)
        }

        // Could make a more optimized one~
        func hexa(prefixed isPrefixed:Bool = true) -> String {
            return self.bytes.reduce(isPrefixed ? "0x" : "") { $0 + String(format: "%02X", $1) }
        }
    }

    print("000204ff5400".data(using: .hexadecimal)?.hexa() ?? "failed") // OK
    print("0x000204ff5400".data(using: .hexadecimal)?.hexa() ?? "failed") // OK
    print("541".data(using: .hexadecimal)?.hexa() ?? "failed") // fails
    print("5413".data(using: .hexadecimal)?.hexa() ?? "failed") // OK

Swift 5

extension Data {
    init?(hex: String) {
        guard hex.count.isMultiple(of: 2) else {
            return nil
        }
        
        let chars = hex.map { $0 }
        let bytes = stride(from: 0, to: chars.count, by: 2)
            .map { String(chars[$0]) + String(chars[$0 + 1]) }
            .compactMap { UInt8($0, radix: 16) }
        
        guard hex.count / bytes.count == 2 else { return nil }
        self.init(bytes)
    }
}

Here's a simple solution I settled on:

extension NSData {
    public convenience init(hexString: String) {
        var index = hexString.startIndex
        var bytes: [UInt8] = []
        repeat {
            bytes.append(hexString[index...index.advancedBy(1)].withCString {
                return UInt8(strtoul($0, nil, 16))
            })

            index = index.advancedBy(2)
        } while index.distanceTo(hexString.endIndex) != 0

        self.init(bytes: &bytes, length: bytes.count)
    }
}

Usage:

let data = NSData(hexString: "b8dfb080bc33fb564249e34252bf143d88fc018f")

Output:

print(data)
>>> <b8dfb080 bc33fb56 4249e342 52bf143d 88fc018f>

Update 6/29/2016

I updated the initializer to handle malformed data (ie, invalid characters or odd number of characters).

public convenience init?(hexString: String, force: Bool) {
    let characterSet = NSCharacterSet(charactersInString: "0123456789abcdefABCDEF")
    for scalar in hexString.unicodeScalars {
        if characterSet.characterIsMember(UInt16(scalar.value)) {
            hexString.append(scalar)
        }
        else if !force {
            return nil
        }
    }

    if hexString.characters.count % 2 == 1 {
        if force {
            hexString = "0" + hexString
        }
        else {
            return nil
        }
    }

    var index = hexString.startIndex
    var bytes: [UInt8] = []
    repeat {
        bytes.append(hexString[index...index.advancedBy(1)].withCString {
            return UInt8(strtoul($0, nil, 16))
            })

        index = index.advancedBy(2)
    } while index.distanceTo(hexString.endIndex) != 0

    self.init(bytes: &bytes, length: bytes.count)
}

Here is my take on converting hexadecimal string to Data using Swift 4:

extension Data {
    private static let hexRegex = try! NSRegularExpression(pattern: "^([a-fA-F0-9][a-fA-F0-9])*$", options: [])

    init?(hexString: String) {
        if Data.hexRegex.matches(in: hexString, range: NSMakeRange(0, hexString.count)).isEmpty {
            return nil // does not look like a hexadecimal string
        }

        let chars = Array(hexString)

        let bytes: [UInt8] = 
            stride(from: 0, to: chars.count, by: 2)
                .map {UInt8(String([chars[$0], chars[$0+1]]), radix: 16)}
                .compactMap{$0}

        self = Data(bytes)
    }

    var hexString: String {
        return map { String(format: "%02hhx", $0) }.joined()
    }
}

(I threw in a small feature for converting back to hex string I found in this answer )

And here is how you would use it:

    let data = Data(hexString: "cafecafe")

    print(data?.hexString) // will print Optional("cafecafe")

One more solution that is simple to follow and leverages swifts built-in hex parsing

func convertHexToBytes(_ str: String) -> Data? {
    let values = str.compactMap { $0.hexDigitValue } // map char to value of 0-15 or nil
    if values.count == str.count && values.count % 2 == 0 {
        var data = Data()
        for x in stride(from: 0, to: values.count, by: 2) {
          let byte = (values[x] << 4) + values[x+1] // concat high and low bits
          data.append(UInt8(byte))
        }
        return data
    }
    return nil
}

let good = "e01AFd"
let bad = "e0671"
let ugly = "GT40"
print("\(convertHexToBytes(good))") // Optional(3 bytes)
print("\(convertHexToBytes(bad))") // nil
print("\(convertHexToBytes(ugly))") // nil

The code worked for me in Swift 3.0.2 .

extension String {
    /// Expanded encoding
    ///
    /// - bytesHexLiteral: Hex string of bytes
    /// - base64: Base64 string
    enum ExpandedEncoding {
        /// Hex string of bytes
        case bytesHexLiteral
        /// Base64 string
        case base64
    }

    /// Convert to `Data` with expanded encoding
    ///
    /// - Parameter encoding: Expanded encoding
    /// - Returns: data
    func data(using encoding: ExpandedEncoding) -> Data? {
        switch encoding {
        case .bytesHexLiteral:
            guard self.characters.count % 2 == 0 else { return nil }
            var data = Data()
            var byteLiteral = ""
            for (index, character) in self.characters.enumerated() {
                if index % 2 == 0 {
                    byteLiteral = String(character)
                } else {
                    byteLiteral.append(character)
                    guard let byte = UInt8(byteLiteral, radix: 16) else { return nil }
                    data.append(byte)
                }
            }
            return data
        case .base64:
            return Data(base64Encoded: self)
        }
    }
}

Swift 5

With support iOS 13 and iOS2 ... iOS12 .

extension String {
  var hex: Data? {
    var value = self
    var data = Data()
    
    while value.count > 0 {
      let subIndex = value.index(value.startIndex, offsetBy: 2)
      let c = String(value[..<subIndex])
      value = String(value[subIndex...])
      
      var char: UInt8
      if #available(iOS 13.0, *) {
        guard let int = Scanner(string: c).scanInt32(representation: .hexadecimal) else { return nil }
        char = UInt8(int)
      } else {
        var int: UInt32 = 0
        Scanner(string: c).scanHexInt32(&int)
        char = UInt8(int)
      }
      
      data.append(&char, count: 1)
    }
    
    return data
  }
}

Swift 5

There is a compact implementation of initialize Data instance from hex string using a regular expression. It searches hex numbers inside a string and combine them to a result data so that it can support different formats of hex representations:

extension Data {
    private static let regex = try! NSRegularExpression(pattern: "([0-9a-fA-F]{2})", options: [])
    
    /// Create instance from string with hex numbers.
    init(from: String) {
        let range = NSRange(location: 0, length: from.utf16.count)
        let bytes = Self.regex.matches(in: from, options: [], range: range)
            .compactMap { Range($0.range(at: 1), in: from) }
            .compactMap { UInt8(from[$0], radix: 16) }
        self.init(bytes)
    }
    
    /// Hex string representation of data.
    var hex: String {
        map { String($0, radix: 16) }.joined()
    }
}

Examples:

let data = Data(from: "0x11223344aabbccdd")
print(data.hex) // Prints "11223344aabbccdd"

let data2 = Data(from: "11223344aabbccdd")
print(data2.hex) // Prints "11223344aabbccdd"

let data3 = Data(from: "11223344 aabbccdd")
print(data3.hex) // Prints "11223344aabbccdd"

let data4 = Data(from: "11223344 AABBCCDD")
print(data4.hex) // Prints "11223344aabbccdd"

let data5 = Data(from: "Hex: 0x11223344AABBCCDD")
print(data5.hex) // Prints "11223344aabbccdd"

let data6 = Data(from: "word[0]=11223344 word[1]=AABBCCDD")
print(data6.hex) // Prints "11223344aabbccdd"

let data7 = Data(from: "No hex")
print(data7.hex) // Prints ""
  • Handles prefixes
  • Ignores invalid characters and incomplete bytes
  • Uses Swift built in hex character parsing
  • Doesn't use subscripts
extension Data {
    init(hexString: String) {
        self = hexString
            .dropFirst(hexString.hasPrefix("0x") ? 2 : 0)
            .compactMap { $0.hexDigitValue.map { UInt8($0) } }
            .reduce(into: (data: Data(capacity: hexString.count / 2), byte: nil as UInt8?)) { partialResult, nibble in
                if let p = partialResult.byte {
                    partialResult.data.append(p + nibble)
                    partialResult.byte = nil
                } else {
                    partialResult.byte = nibble << 4
                }
            }.data
    }
}

Supposing your string is even size, you can use this to convert to hexadecimal and save it to Data:

Swift 5.2

func hex(from string: String) -> Data {
    .init(stride(from: 0, to: string.count, by: 2).map {
        string[string.index(string.startIndex, offsetBy: $0) ... string.index(string.startIndex, offsetBy: $0 + 1)]
    }.map {
        UInt8($0, radix: 16)!
    })
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM