diff --git a/Sources/ULID/Data+Base32.swift b/Sources/ULID/Data+Base32.swift index 7f09daa..15855a4 100644 --- a/Sources/ULID/Data+Base32.swift +++ b/Sources/ULID/Data+Base32.swift @@ -9,41 +9,26 @@ import Foundation enum Base32 { - static let crockfordsEncodingTable: [Character] = Array("0123456789ABCDEFGHJKMNPQRSTVWXYZ") + static let crockfordsEncodingTable: [UInt8] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".utf8.map({ $0 }) - static let crockfordsDecodingTable: [Character: UInt8] = [ - "0": 0x00, "O": 0x00, "o": 0x00, - "1": 0x01, "I": 0x01, "i": 0x01, "L": 0x01, "l": 0x01, - "2": 0x02, - "3": 0x03, - "4": 0x04, - "5": 0x05, - "6": 0x06, - "7": 0x07, - "8": 0x08, - "9": 0x09, - "A": 0x0a, "a": 0x0a, - "B": 0x0b, "b": 0x0b, - "C": 0x0c, "c": 0x0c, - "D": 0x0d, "d": 0x0d, - "E": 0x0e, "e": 0x0e, - "F": 0x0f, "f": 0x0f, - "G": 0x10, "g": 0x10, - "H": 0x11, "h": 0x11, - "J": 0x12, "j": 0x12, - "K": 0x13, "k": 0x13, - "M": 0x14, "m": 0x14, - "N": 0x15, "n": 0x15, - "P": 0x16, "p": 0x16, - "Q": 0x17, "q": 0x17, - "R": 0x18, "r": 0x18, - "S": 0x19, "s": 0x19, - "T": 0x1a, "t": 0x1a, - "V": 0x1b, "v": 0x1b, - "W": 0x1c, "w": 0x1c, - "X": 0x1d, "x": 0x1d, - "Y": 0x1e, "y": 0x1e, - "Z": 0x1f, "z": 0x1f + static let crockfordsDecodingTable: [UInt8] = [ + // 0 1 2 3 4 5 6 7 8 9 a b c d e f + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 1 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 2 + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 3 + 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x01, 0x12, 0x13, 0x01, 0x14, 0x15, 0x00, // 4 + 0x16, 0x17, 0x18, 0x19, 0x1a, 0xff, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, // 5 + 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x01, 0x12, 0x13, 0x01, 0x14, 0x15, 0x00, // 6 + 0x16, 0x17, 0x18, 0x19, 0x1a, 0xff, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, // 7 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 8 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 9 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // a + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // b + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // c + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // d + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // e + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff // f ] } @@ -55,37 +40,80 @@ enum Base32Error: Error { extension Data { /// Decode Crockford's Base32 - init?(base32Encoded base32String: String, using table: [Character: UInt8] = Base32.crockfordsDecodingTable) { - var str: [Character] = Array(base32String) - while let last = str.last, last == "=" { - str.removeLast() + init?(base32Encoded base32String: String, using table: [UInt8] = Base32.crockfordsDecodingTable) { + guard base32String.unicodeScalars.allSatisfy({ $0.isASCII }) else { + return nil } - let div = str.count / 8 - let mod = str.count % 8 + var base32String = base32String + while let last = base32String.last, last == "=" { + base32String.removeLast() + } + guard [0, 2, 4, 5, 7].contains(base32String.count % 8) else { + return nil + } - var buffer = Data() + let dstlen = base32String.count * 5 / 8 - do { - func unwrap(_ value: UInt8?) throws -> UInt8 { - guard let value = value else { throw Base32Error.invalidCharacter } - return value + var buffer = Data(count: dstlen) + + let success: Bool = buffer.withUnsafeMutableBytes { (dst: UnsafeMutablePointer) in + base32String.withCString(encodedAs: Unicode.ASCII.self) { (src) in + func _strlen(_ str: UnsafePointer) -> Int { + var str = str + var i = 0 + while str.pointee != 0 { + str += 1 + i += 1 + } + return i + } + + var srcleft = _strlen(src) + var srcp = src + + var dstp = dst + + let work = UnsafeMutablePointer.allocate(capacity: 8) + defer { work.deallocate() } + + while srcleft > 0 { + let worklen = Swift.min(8, srcleft) + for i in 0 ..< worklen { + work[i] = table[Int(srcp[i])] + if work[i] == 0xff { + return false + } + } + + switch worklen { + case 8: + dstp[4] = (work[6] << 5) | (work[7] ) + fallthrough + case 7: + dstp[3] = (work[4] << 7) | (work[5] << 2) | (work[6] >> 3) + fallthrough + case 5: + dstp[2] = (work[3] << 4) | (work[4] >> 1) + fallthrough + case 4: + dstp[1] = (work[1] << 6) | (work[2] << 1) | (work[3] >> 4) + fallthrough + case 2: + dstp[0] = (work[0] << 3) | (work[1] >> 2) + default: + break + } + + srcp += 8 + srcleft -= 8 + dstp += 5 + } + + return true } - - for i in 0 ... div { - if i == div, mod == 0 { break } - let offset = 8 * i - try buffer.append(unwrap(table[str[offset + 0]]) << 3 | unwrap(table[str[offset + 1]]) >> 2) - if i == div, mod == 2 { break } - try buffer.append(unwrap(table[str[offset + 1]]) << 6 | unwrap(table[str[offset + 2]]) << 1 | unwrap(table[str[offset + 3]]) >> 4) - if i == div, mod == 4 { break } - try buffer.append(unwrap(table[str[offset + 3]]) << 4 | unwrap(table[str[offset + 4]]) >> 1) - if i == div, mod == 5 { break } - try buffer.append(unwrap(table[str[offset + 4]]) << 7 | unwrap(table[str[offset + 5]]) << 2 | unwrap(table[str[offset + 6]]) >> 3) - if i == div, mod == 7 { break } - try buffer.append(unwrap(table[str[offset + 6]]) << 5 | unwrap(table[str[offset + 7]])) - } - } catch { + } + guard success else { return nil } @@ -93,36 +121,89 @@ extension Data { } /// Encode Crockford's Base32 - func base32EncodedString(using table: [Character] = Base32.crockfordsEncodingTable) -> String { - let div = self.count / 5 - let mod = self.count % 5 + func base32EncodedString(padding: Bool = true, using table: [UInt8] = Base32.crockfordsEncodingTable) -> String { + var srcleft = self.count - return self.withUnsafeBytes { (bytes: UnsafePointer) in - var str = [Character]() - var pad = 0 + let dstlen: Int + if padding { + dstlen = (self.count + 4) / 5 * 8 + } else { + dstlen = (self.count * 8 + 4) / 5 + } + var dstleft = dstlen - for i in 0 ... div { - if i == div, mod == 0 { break } - let offset = 5 * i - str.append(table[Int((bytes[offset + 0] >> 3))]) - str.append(table[Int((bytes[offset + 0] & 0b00000111) << 2 | bytes[offset + 1] >> 6)]) - if i == div, mod == 1 { pad = 6; break } - str.append(table[Int((bytes[offset + 1] & 0b00111110) >> 1)]) - str.append(table[Int((bytes[offset + 1] & 0b00000001) << 4 | bytes[offset + 2] >> 4)]) - if i == div, mod == 2 { pad = 4; break } - str.append(table[Int((bytes[offset + 2] & 0b00001111) << 1 | bytes[offset + 3] >> 7)]) - if i == div, mod == 3 { pad = 3; break } - str.append(table[Int((bytes[offset + 3] & 0b01111100) >> 2)]) - str.append(table[Int((bytes[offset + 3] & 0b00000011) << 3 | bytes[offset + 4] >> 5)]) - if i == div, mod == 4 { pad = 1; break } - str.append(table[Int((bytes[offset + 4] & 0b00011111))]) + return self.withUnsafeBytes { (src: UnsafePointer) in + var srcp = src + + let dst = UnsafeMutablePointer.allocate(capacity: dstlen + 1) + var dstp = dst + defer { dst.deallocate() } + + let work = UnsafeMutablePointer.allocate(capacity: 8) + defer { work.deallocate() } + + while srcleft > 0 { + switch srcleft { + case _ where 5 <= srcleft: + work[7] = srcp[4] + work[6] = srcp[4] >> 5 + fallthrough + case 4: + work[6] |= srcp[3] << 3 + work[5] = srcp[3] >> 2 + work[4] = srcp[3] >> 7 + fallthrough + case 3: + work[4] |= srcp[2] << 1 + work[3] = srcp[2] >> 4 + fallthrough + case 2: + work[3] |= srcp[1] << 4 + work[2] = srcp[1] >> 1 + work[1] = srcp[1] >> 6 + fallthrough + case 1: + work[1] |= srcp[0] << 2 + work[0] = srcp[0] >> 3 + default: + break + } + + for i in 0 ..< Swift.min(8, dstleft) { + dstp[i] = table[Int(work[i] & 0x1f)] + } + + if srcleft < 5 { + if padding { + switch srcleft { + case 1: + dstp[2] = "=".utf8.first! + dstp[3] = "=".utf8.first! + fallthrough + case 2: + dstp[4] = "=".utf8.first! + fallthrough + case 3: + dstp[5] = "=".utf8.first! + dstp[6] = "=".utf8.first! + fallthrough + case 4: + dstp[7] = "=".utf8.first! + default: + break + } + } + break + } + + srcp += 5 + srcleft -= 5 + dstp += 8 + dstleft -= 8 } - for _ in 0 ..< pad { - str.append("=") - } - - return String(str) + dst[dstlen] = 0 + return String(cString: dst) } } diff --git a/Tests/ULIDTests/Data+Base32Tests.swift b/Tests/ULIDTests/Data+Base32Tests.swift index c27cbf9..f4220a4 100644 --- a/Tests/ULIDTests/Data+Base32Tests.swift +++ b/Tests/ULIDTests/Data+Base32Tests.swift @@ -10,6 +10,9 @@ import XCTest final class Base32Tests: XCTestCase { + // MARK: - + // MARK: Encode + func testEncodeBase32() { let expected = "00000001D0YX86C6ZTSZJNXFHDQMCYBQ" @@ -22,6 +25,117 @@ final class Base32Tests: XCTestCase { XCTAssertEqual(expected, data.base32EncodedString()) } + func testEncode1() { + let bytes: [UInt8] = [ + 0b11111000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("Z0000000", data.base32EncodedString()) + } + + func testEncode2() { + let bytes: [UInt8] = [ + 0b00000111, 0b11000000, 0b00000000, 0b00000000, 0b00000000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("0Z000000", data.base32EncodedString()) + } + + func testEncode3() { + let bytes: [UInt8] = [ + 0b00000000, 0b00111110, 0b00000000, 0b00000000, 0b00000000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("00Z00000", data.base32EncodedString()) + } + + func testEncode4() { + let bytes: [UInt8] = [ + 0b00000000, 0b00000001, 0b11110000, 0b00000000, 0b00000000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("000Z0000", data.base32EncodedString()) + } + + func testEncode5() { + let bytes: [UInt8] = [ + 0b00000000, 0b00000000, 0b00001111, 0b10000000, 0b00000000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("0000Z000", data.base32EncodedString()) + } + + func testEncode6() { + let bytes: [UInt8] = [ + 0b00000000, 0b00000000, 0b00000000, 0b01111100, 0b00000000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("00000Z00", data.base32EncodedString()) + } + + func testEncode7() { + let bytes: [UInt8] = [ + 0b00000000, 0b00000000, 0b00000000, 0b00000011, 0b11100000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("000000Z0", data.base32EncodedString()) + } + + func testEncode8() { + let bytes: [UInt8] = [ + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00011111 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("0000000Z", data.base32EncodedString()) + } + + func testEncodePad1() { + let bytes: [UInt8] = [ + 0b10000100 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("GG======", data.base32EncodedString()) + } + + func testEncodePad2() { + let bytes: [UInt8] = [ + 0b10000100, 0b00100001 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("GGGG====", data.base32EncodedString()) + } + + func testEncodePad3() { + let bytes: [UInt8] = [ + 0b10000100, 0b00100001, 0b00001000 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("GGGGG===", data.base32EncodedString()) + } + + func testEncodePad4() { + let bytes: [UInt8] = [ + 0b10000100, 0b00100001, 0b00001000, 0b01000010 + ] + let data = Data(bytes: bytes) + + XCTAssertEqual("GGGGGGG=", data.base32EncodedString()) + } + + // MARK: - + // MARK: Decode + func testDecodeBase32() { let expected: [UInt8] = [ 0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x3D, 0xD4, 0x19, 0x86, @@ -35,6 +149,52 @@ final class Base32Tests: XCTestCase { XCTAssertEqual(expected, Array(data!)) } + func testDecodeTable() { + let table: [Character: UInt8] = [ + "0": 0x00, "O": 0x00, "o": 0x00, + "1": 0x01, "I": 0x01, "i": 0x01, "L": 0x01, "l": 0x01, + "2": 0x02, + "3": 0x03, + "4": 0x04, + "5": 0x05, + "6": 0x06, + "7": 0x07, + "8": 0x08, + "9": 0x09, + "A": 0x0a, "a": 0x0a, + "B": 0x0b, "b": 0x0b, + "C": 0x0c, "c": 0x0c, + "D": 0x0d, "d": 0x0d, + "E": 0x0e, "e": 0x0e, + "F": 0x0f, "f": 0x0f, + "G": 0x10, "g": 0x10, + "H": 0x11, "h": 0x11, + "J": 0x12, "j": 0x12, + "K": 0x13, "k": 0x13, + "M": 0x14, "m": 0x14, + "N": 0x15, "n": 0x15, + "P": 0x16, "p": 0x16, + "Q": 0x17, "q": 0x17, + "R": 0x18, "r": 0x18, + "S": 0x19, "s": 0x19, + "T": 0x1a, "t": 0x1a, + "V": 0x1b, "v": 0x1b, + "W": 0x1c, "w": 0x1c, + "X": 0x1d, "x": 0x1d, + "Y": 0x1e, "y": 0x1e, + "Z": 0x1f, "z": 0x1f + ] + + for (char, value) in table { + let data = Data(base32Encoded: String(char) + "0") + XCTAssertNotNil(data) + XCTAssertEqual(1, data!.count) + XCTAssertEqual(value << 3, data![0]) + } + } + + // MARK: - + static var allTests = [ ("testEncodeBase32", testEncodeBase32), ("testDecodeBase32", testDecodeBase32)