Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
yaslab committed Jul 14, 2024
1 parent acdd675 commit 2b6a587
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 35 deletions.
54 changes: 54 additions & 0 deletions Sources/Hex/Base16.swift
Original file line number Diff line number Diff line change
@@ -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<I: IteratorProtocol>(_ 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)
)
)
}
}
42 changes: 19 additions & 23 deletions Sources/Hex/Data+Hex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<S: Sequence>(_ 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)
}
}
13 changes: 10 additions & 3 deletions Sources/Hex/Sequence+Hex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
19 changes: 15 additions & 4 deletions Tests/HexTests/Data+HexTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 16 additions & 5 deletions Tests/HexTests/Sequence+HexTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 2b6a587

Please sign in to comment.