From 2b6a5874cad3f8bfc3ff98a7714c62f350da82d2 Mon Sep 17 00:00:00 2001 From: Yasuhiro Hatta Date: Sun, 14 Jul 2024 18:39:28 +0900 Subject: [PATCH] Refactoring --- Sources/Hex/Base16.swift | 54 ++++++++++++++++++++++++++ Sources/Hex/Data+Hex.swift | 42 +++++++++----------- Sources/Hex/Sequence+Hex.swift | 13 +++++-- Tests/HexTests/Data+HexTests.swift | 19 +++++++-- Tests/HexTests/Sequence+HexTests.swift | 21 +++++++--- 5 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 Sources/Hex/Base16.swift diff --git a/Sources/Hex/Base16.swift b/Sources/Hex/Base16.swift new file mode 100644 index 0000000..9c6bcc4 --- /dev/null +++ b/Sources/Hex/Base16.swift @@ -0,0 +1,54 @@ +import Foundation + +enum Base16 { + enum DecodingResult: Equatable, Sendable { + case emptyInput + case error + case hexValue(UInt8) + } + + /// Decodes hex character into 4-bit data. + static func decode(_ input: inout I) -> DecodingResult where I.Element == UInt8 { + func hex(from char: UInt8) -> UInt8? { + if 0x30 <= char, char <= 0x39 { // '0'-'9' + return char - 0x30 + } else if 0x41 <= char, char <= 0x46 { // 'A'-'F' + return char - 0x41 + 10 + } else if 0x61 <= char, char <= 0x66 { // 'a'-'f' + return char - 0x61 + 10 + } + return nil + } + + guard let next0 = input.next() else { + return .emptyInput + } + guard let next1 = input.next() else { + return .error + } + + guard let upper = hex(from: next0), let lower = hex(from: next1) else { + return .error + } + + return .hexValue((upper << 4) | lower) + } + + /// Encodes 8-bit data into hex characters. + static func encode(_ input: UInt8, into process: ((upper: UInt8, lower: UInt8)) -> Void) { + func char(from hex: UInt8) -> UInt8 { + if 0 <= hex, hex <= 9 { + return 0x30 + hex // '0'-'9' + } else { + return 0x61 + hex - 10 // 'a'-'f' + } + } + + process( + ( + upper: char(from: input >> 4), + lower: char(from: input & 0b1111) + ) + ) + } +} diff --git a/Sources/Hex/Data+Hex.swift b/Sources/Hex/Data+Hex.swift index 67a4d6c..4d20417 100644 --- a/Sources/Hex/Data+Hex.swift +++ b/Sources/Hex/Data+Hex.swift @@ -5,40 +5,36 @@ extension Data { /// /// - Parameter hexData: The Hexadecimal encoded data. public init?(hexEncoded hexData: Data) { - guard let hexString = String(data: hexData, encoding: .utf8) else { + guard let data = Data.decode(hexData) else { return nil } - - self.init(hexEncoded: hexString) + self = data } /// Creates data with the Hexadecimal (also known as Base-16) encoded string. /// /// - Parameter hexString: The Hexadecimal encoded string. public init?(hexEncoded hexString: String) { - var data = Data() - var upper = true + var hexString = hexString + guard let data = hexString.withUTF8(Data.decode(_:)) else { + return nil + } + self = data + } - for char in hexString { - guard char.isASCII, let hex = char.hexDigitValue else { + private static func decode(_ input: S) -> Data? where S.Element == UInt8 { + var bytes = [UInt8]() + var it = input.makeIterator() + EOF: while true { + switch Base16.decode(&it) { + case .emptyInput: + break EOF + case .error: return nil + case .hexValue(let byte): + bytes.append(byte) } - - if upper { - // append upper 4bits - data.append(UInt8(hex) << 4) - } else { - // append lower 4bits - data[data.count - 1] += UInt8(hex) - } - - upper.toggle() } - - if !upper { - return nil - } - - self = data + return Data(bytes) } } diff --git a/Sources/Hex/Sequence+Hex.swift b/Sources/Hex/Sequence+Hex.swift index 149941c..04bb188 100644 --- a/Sources/Hex/Sequence+Hex.swift +++ b/Sources/Hex/Sequence+Hex.swift @@ -3,13 +3,20 @@ import Foundation extension Sequence where Element == UInt8 { /// Returns the Hexadecimal (also known as Base-16) encoded data. public func hexEncodedData() -> Data { - hexEncodedString().data(using: .utf8)! + Data(encode()) } /// Returns the Hexadecimal (also known as Base-16) encoded string. public func hexEncodedString() -> String { - reduce(into: "") { hexString, byte in - hexString += String(format: "%02x", byte) + String(decoding: encode(), as: UTF8.self) + } + + private func encode() -> [UInt8] { + reduce(into: [UInt8]()) { string, byte in + Base16.encode(byte) { result in + string.append(result.upper) + string.append(result.lower) + } } } } diff --git a/Tests/HexTests/Data+HexTests.swift b/Tests/HexTests/Data+HexTests.swift index cd96979..94b9dad 100644 --- a/Tests/HexTests/Data+HexTests.swift +++ b/Tests/HexTests/Data+HexTests.swift @@ -3,26 +3,37 @@ import Hex import Testing struct DataHexTests { + @Test func decodeHexData() throws { + // Arrange + let hexData = try #require("000123456789abcdefff".data(using: .utf8)) + + // Act + let data = try #require(Data(hexEncoded: hexData)) + + // Assert + #expect(data == Data([0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xff])) + } + @Test func decodeHexString() throws { // Arrange - let hexString = "0001feff" + let hexString = "000123456789abcdefff" // Act let data = try #require(Data(hexEncoded: hexString)) // Assert - #expect(data == Data([0x00, 0x01, 0xfe, 0xff])) + #expect(data == Data([0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xff])) } @Test func decodeUpperCase() throws { // Arrange - let hexString = "303132333435363738394a4b4C4D" + let hexString = "30313233344a4b4C4D" // Act let data = try #require(Data(hexEncoded: hexString)) // Assert - #expect(String(decoding: data, as: UTF8.self) == "0123456789JKLM") + #expect(String(decoding: data, as: UTF8.self) == "01234JKLM") } @Test func decodeEmpty() throws { diff --git a/Tests/HexTests/Sequence+HexTests.swift b/Tests/HexTests/Sequence+HexTests.swift index 36560bb..1f7795a 100644 --- a/Tests/HexTests/Sequence+HexTests.swift +++ b/Tests/HexTests/Sequence+HexTests.swift @@ -3,26 +3,37 @@ import Hex import Testing struct SequenceHexTests { - @Test func encodeByteArray() { + @Test func encodeIntoData() { // Arrange - let bytes: [UInt8] = [0x00, 0x01, 0xfe, 0xff] + let bytes: [UInt8] = [0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xff] + + // Act + let hexData = bytes.hexEncodedData() + + // Assert + #expect(hexData == "000123456789abcdefff".data(using: .utf8)) + } + + @Test func encodeIntoString() { + // Arrange + let bytes: [UInt8] = [0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xff] // Act let hexString = bytes.hexEncodedString() // Assert - #expect(hexString == "0001feff") + #expect(hexString == "000123456789abcdefff") } @Test func encodeStringData() throws { // Arrange - let data = try #require("0123456789JKLM".data(using: .utf8)) + let data = try #require("01234JKLM".data(using: .utf8)) // Act let hexString = data.hexEncodedString() // Assert - #expect(hexString == "303132333435363738394a4b4c4d") + #expect(hexString == "30313233344a4b4c4d") } @Test func encodeEmpty() {