mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 04:25:21 +00:00
Data.hexEncodedString() built each byte via String(format: "%02x", _), paying Foundation format-string parsing per byte on the hot BLE receive path (PeerID(hexData:) is called several times per received packet). Replace it with an ASCII lookup table writing into a preallocated buffer; output is byte-for-byte identical (lowercase hex). Data(hexString:) similarly allocated a two-character substring and went through integer radix parsing per byte; replace with direct nibble lookups over the UTF-8 bytes. Semantics are unchanged except that sign-prefixed pairs like "+f", which the old radix parser incorrectly accepted, are now rejected. Add round-trip, known-vector, and invalid-input tests to BitFoundationTests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
2.7 KiB
Swift
90 lines
2.7 KiB
Swift
//
|
|
// Data+Hex.swift
|
|
// BitFoundation
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import struct Foundation.Data
|
|
|
|
/// Lowercase hex digits used by `hexEncodedString()`.
|
|
private let hexDigits: [UInt8] = Array("0123456789abcdef".utf8)
|
|
|
|
/// Maps an ASCII byte to its hex nibble value, or nil for non-hex characters.
|
|
/// Accepts both lowercase and uppercase hex digits.
|
|
@inline(__always)
|
|
private func hexNibble(_ ascii: UInt8) -> UInt8? {
|
|
switch ascii {
|
|
case UInt8(ascii: "0")...UInt8(ascii: "9"):
|
|
return ascii - UInt8(ascii: "0")
|
|
case UInt8(ascii: "a")...UInt8(ascii: "f"):
|
|
return ascii - UInt8(ascii: "a") + 10
|
|
case UInt8(ascii: "A")...UInt8(ascii: "F"):
|
|
return ascii - UInt8(ascii: "A") + 10
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public extension Data {
|
|
/// Lowercase hex representation of the bytes.
|
|
///
|
|
/// Lookup-table based: this sits on the hot BLE receive path (it is called
|
|
/// several times per received packet via `PeerID(hexData:)`), where the
|
|
/// previous per-byte `String(format: "%02x", _)` implementation spent most
|
|
/// of its time re-parsing the format string through Foundation.
|
|
func hexEncodedString() -> String {
|
|
if isEmpty {
|
|
return ""
|
|
}
|
|
var output = [UInt8](repeating: 0, count: count * 2)
|
|
var i = 0
|
|
for byte in self {
|
|
output[i] = hexDigits[Int(byte >> 4)]
|
|
output[i + 1] = hexDigits[Int(byte & 0x0F)]
|
|
i += 2
|
|
}
|
|
return String(decoding: output, as: UTF8.self)
|
|
}
|
|
|
|
/// Initialize Data from a hex string.
|
|
/// - Parameter hexString: A hex string, optionally prefixed with "0x" or "0X".
|
|
/// Whitespace is trimmed. Must have even length after prefix removal.
|
|
/// - Returns: nil if the string has odd length or contains invalid hex characters.
|
|
init?(hexString: String) {
|
|
var hex = hexString.trimmed
|
|
|
|
// Remove optional 0x prefix
|
|
if hex.hasPrefix("0x") || hex.hasPrefix("0X") {
|
|
hex = String(hex.dropFirst(2))
|
|
}
|
|
|
|
let ascii = Array(hex.utf8)
|
|
|
|
// Reject odd-length strings
|
|
guard ascii.count % 2 == 0 else {
|
|
return nil
|
|
}
|
|
|
|
// Accept empty strings
|
|
guard !ascii.isEmpty else {
|
|
self = Data()
|
|
return
|
|
}
|
|
|
|
var data = Data(capacity: ascii.count / 2)
|
|
var index = 0
|
|
while index < ascii.count {
|
|
guard let high = hexNibble(ascii[index]),
|
|
let low = hexNibble(ascii[index + 1]) else {
|
|
return nil
|
|
}
|
|
data.append((high << 4) | low)
|
|
index += 2
|
|
}
|
|
|
|
self = data
|
|
}
|
|
}
|