Files
bitchat/bitchat/Protocols/BinaryEncodingUtils.swift
T
evgeniy.chernomortsev 6efe9d02fb fix: harden hex string parsing (#646)
Improve Data(hexString:) to handle edge cases:
- Reject odd-length strings (return nil)
- Support optional 0x/0X prefix
- Trim leading/trailing whitespace
- Handle empty strings correctly

Add comprehensive unit tests for hex parsing.
2025-12-09 15:01:59 +04:00

251 lines
7.3 KiB
Swift

//
// BinaryEncodingUtils.swift
// bitchat
//
// Binary encoding utilities for efficient protocol messages
//
import Foundation
import CryptoKit
// MARK: - Hex Encoding/Decoding
extension Data {
func hexEncodedString() -> String {
if self.isEmpty {
return ""
}
return self.map { String(format: "%02x", $0) }.joined()
}
func sha256Hex() -> String {
let digest = SHA256.hash(data: self)
return digest.map { String(format: "%02x", $0) }.joined()
}
/// 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.trimmingCharacters(in: .whitespaces)
// Remove optional 0x prefix
if hex.hasPrefix("0x") || hex.hasPrefix("0X") {
hex = String(hex.dropFirst(2))
}
// Reject odd-length strings
guard hex.count % 2 == 0 else {
return nil
}
// Reject empty strings
guard !hex.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 {
return nil
}
data.append(byte)
index = nextIndex
}
self = data
}
}
// MARK: - Binary Encoding Utilities
extension Data {
// MARK: Writing
@inlinable mutating func appendUInt8(_ value: UInt8) {
self.append(value)
}
@inlinable mutating func appendUInt16(_ value: UInt16) {
self.append(UInt8((value >> 8) & 0xFF))
self.append(UInt8(value & 0xFF))
}
@inlinable mutating func appendUInt32(_ value: UInt32) {
self.append(UInt8((value >> 24) & 0xFF))
self.append(UInt8((value >> 16) & 0xFF))
self.append(UInt8((value >> 8) & 0xFF))
self.append(UInt8(value & 0xFF))
}
@inlinable mutating func appendUInt64(_ value: UInt64) {
for i in (0..<8).reversed() {
self.append(UInt8((value >> (i * 8)) & 0xFF))
}
}
mutating func appendString(_ string: String, maxLength: Int = 255) {
guard let data = string.data(using: .utf8) else { return }
let length = Swift.min(data.count, maxLength)
if maxLength <= 255 {
self.append(UInt8(length))
} else {
self.appendUInt16(UInt16(length))
}
self.append(data.prefix(length))
}
mutating func appendData(_ data: Data, maxLength: Int = 65535) {
let length = Swift.min(data.count, maxLength)
if maxLength <= 255 {
self.append(UInt8(length))
} else {
self.appendUInt16(UInt16(length))
}
self.append(data.prefix(length))
}
mutating func appendDate(_ date: Date) {
let timestamp = UInt64(date.timeIntervalSince1970 * 1000) // milliseconds
self.appendUInt64(timestamp)
}
mutating func appendUUID(_ uuid: String) {
// Convert UUID string to 16 bytes
var uuidData = Data(count: 16)
let cleanUUID = uuid.replacingOccurrences(of: "-", with: "")
var index = cleanUUID.startIndex
for i in 0..<16 {
guard index < cleanUUID.endIndex else { break }
let nextIndex = cleanUUID.index(index, offsetBy: 2)
if let byte = UInt8(String(cleanUUID[index..<nextIndex]), radix: 16) {
uuidData[i] = byte
}
index = nextIndex
}
self.append(uuidData)
}
// MARK: Reading
@inlinable func readUInt8(at offset: inout Int) -> UInt8? {
guard offset >= 0 && offset < self.count else { return nil }
let value = self[offset]
offset += 1
return value
}
@inlinable func readUInt16(at offset: inout Int) -> UInt16? {
guard offset + 2 <= self.count else { return nil }
let value = UInt16(self[offset]) << 8 | UInt16(self[offset + 1])
offset += 2
return value
}
@inlinable func readUInt32(at offset: inout Int) -> UInt32? {
guard offset + 4 <= self.count else { return nil }
let value = UInt32(self[offset]) << 24 |
UInt32(self[offset + 1]) << 16 |
UInt32(self[offset + 2]) << 8 |
UInt32(self[offset + 3])
offset += 4
return value
}
@inlinable func readUInt64(at offset: inout Int) -> UInt64? {
guard offset + 8 <= self.count else { return nil }
var value: UInt64 = 0
for i in 0..<8 {
value = (value << 8) | UInt64(self[offset + i])
}
offset += 8
return value
}
func readString(at offset: inout Int, maxLength: Int = 255) -> String? {
let length: Int
if maxLength <= 255 {
guard let len = readUInt8(at: &offset) else { return nil }
length = Int(len)
} else {
guard let len = readUInt16(at: &offset) else { return nil }
length = Int(len)
}
guard offset + length <= self.count else { return nil }
let stringData = self[offset..<offset + length]
offset += length
return String(data: stringData, encoding: .utf8)
}
func readData(at offset: inout Int, maxLength: Int = 65535) -> Data? {
let length: Int
if maxLength <= 255 {
guard let len = readUInt8(at: &offset) else { return nil }
length = Int(len)
} else {
guard let len = readUInt16(at: &offset) else { return nil }
length = Int(len)
}
guard offset + length <= self.count else { return nil }
let data = self[offset..<offset + length]
offset += length
return data
}
func readDate(at offset: inout Int) -> Date? {
guard let timestamp = readUInt64(at: &offset) else { return nil }
return Date(timeIntervalSince1970: Double(timestamp) / 1000.0)
}
func readUUID(at offset: inout Int) -> String? {
guard offset + 16 <= self.count else { return nil }
let uuidData = self[offset..<offset + 16]
offset += 16
// Convert 16 bytes to UUID string format
let uuid = uuidData.hexEncodedString()
// Insert hyphens at proper positions: 8-4-4-4-12
var result = ""
for (index, char) in uuid.enumerated() {
if index == 8 || index == 12 || index == 16 || index == 20 {
result += "-"
}
result.append(char)
}
return result.uppercased()
}
func readFixedBytes(at offset: inout Int, count: Int) -> Data? {
guard offset + count <= self.count else { return nil }
let data = self[offset..<offset + count]
offset += count
return data
}
}