mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Speed up hex encode/decode with lookup tables
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>
This commit is contained in:
@@ -8,12 +8,44 @@
|
||||
|
||||
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 self.isEmpty {
|
||||
if isEmpty {
|
||||
return ""
|
||||
}
|
||||
return self.map { String(format: "%02x", $0) }.joined()
|
||||
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.
|
||||
@@ -28,28 +60,28 @@ public extension Data {
|
||||
hex = String(hex.dropFirst(2))
|
||||
}
|
||||
|
||||
let ascii = Array(hex.utf8)
|
||||
|
||||
// Reject odd-length strings
|
||||
guard hex.count % 2 == 0 else {
|
||||
guard ascii.count % 2 == 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject empty strings
|
||||
guard !hex.isEmpty else {
|
||||
// Accept empty strings
|
||||
guard !ascii.isEmpty else {
|
||||
self = Data()
|
||||
return
|
||||
}
|
||||
|
||||
let len = hex.count / 2
|
||||
var data = Data(capacity: len)
|
||||
var index = hex.startIndex
|
||||
|
||||
for _ in 0..<len {
|
||||
let nextIndex = hex.index(index, offsetBy: 2)
|
||||
guard let byte = UInt8(String(hex[index..<nextIndex]), radix: 16) else {
|
||||
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(byte)
|
||||
index = nextIndex
|
||||
data.append((high << 4) | low)
|
||||
index += 2
|
||||
}
|
||||
|
||||
self = data
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// DataHexTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import BitFoundation
|
||||
|
||||
struct DataHexTests {
|
||||
|
||||
// MARK: - Encoding
|
||||
|
||||
@Test func encode_knownVectors() {
|
||||
#expect(Data().hexEncodedString() == "")
|
||||
#expect(Data([0x00]).hexEncodedString() == "00")
|
||||
#expect(Data([0x0f]).hexEncodedString() == "0f")
|
||||
#expect(Data([0xf0]).hexEncodedString() == "f0")
|
||||
#expect(Data([0xff]).hexEncodedString() == "ff")
|
||||
#expect(Data([0xde, 0xad, 0xbe, 0xef]).hexEncodedString() == "deadbeef")
|
||||
#expect(Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]).hexEncodedString() == "0123456789abcdef")
|
||||
}
|
||||
|
||||
@Test func encode_allByteValues_matchesFormatReference() {
|
||||
let all = Data((0...255).map { UInt8($0) })
|
||||
let reference = (0...255).map { String(format: "%02x", $0) }.joined()
|
||||
#expect(all.hexEncodedString() == reference)
|
||||
}
|
||||
|
||||
@Test func encode_worksOnDataSlices() {
|
||||
let data = Data([0xaa, 0xde, 0xad, 0xbe, 0xef, 0xbb])
|
||||
let slice = data.dropFirst().dropLast()
|
||||
#expect(slice.hexEncodedString() == "deadbeef")
|
||||
}
|
||||
|
||||
// MARK: - Decoding
|
||||
|
||||
@Test func decode_knownVectors() {
|
||||
#expect(Data(hexString: "deadbeef") == Data([0xde, 0xad, 0xbe, 0xef]))
|
||||
#expect(Data(hexString: "DEADBEEF") == Data([0xde, 0xad, 0xbe, 0xef]))
|
||||
#expect(Data(hexString: "DeAdBeEf") == Data([0xde, 0xad, 0xbe, 0xef]))
|
||||
#expect(Data(hexString: "00") == Data([0x00]))
|
||||
#expect(Data(hexString: "0123456789abcdef") == Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]))
|
||||
}
|
||||
|
||||
@Test func decode_handlesPrefixAndWhitespace() {
|
||||
#expect(Data(hexString: "0xdeadbeef") == Data([0xde, 0xad, 0xbe, 0xef]))
|
||||
#expect(Data(hexString: "0XDEADBEEF") == Data([0xde, 0xad, 0xbe, 0xef]))
|
||||
#expect(Data(hexString: " deadbeef\n") == Data([0xde, 0xad, 0xbe, 0xef]))
|
||||
#expect(Data(hexString: "") == Data())
|
||||
#expect(Data(hexString: "0x") == Data())
|
||||
}
|
||||
|
||||
@Test func decode_rejectsInvalidInput() {
|
||||
#expect(Data(hexString: "abc") == nil) // odd length
|
||||
#expect(Data(hexString: "zz") == nil) // non-hex characters
|
||||
#expect(Data(hexString: "0xg1") == nil) // non-hex after prefix
|
||||
#expect(Data(hexString: "+f") == nil) // sign characters are not hex
|
||||
#expect(Data(hexString: "-0") == nil)
|
||||
#expect(Data(hexString: "a\u{00e9}") == nil) // non-ASCII
|
||||
#expect(Data(hexString: "de ad") == nil) // interior whitespace
|
||||
}
|
||||
|
||||
// MARK: - Round trip
|
||||
|
||||
@Test func roundTrip_randomLengths() {
|
||||
for length in [0, 1, 2, 3, 8, 16, 31, 32, 33, 64, 255, 1024] {
|
||||
let data = Data((0..<length).map { _ in UInt8.random(in: .min ... .max) })
|
||||
let hex = data.hexEncodedString()
|
||||
#expect(hex.count == length * 2)
|
||||
#expect(Data(hexString: hex) == data)
|
||||
#expect(Data(hexString: hex.uppercased()) == data)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user