Refactor Base32 encoder / decoder

This commit is contained in:
Yasuhiro Hatta 2019-01-13 02:44:06 +09:00
parent c0d5346d88
commit 7664bd83d9
2 changed files with 327 additions and 86 deletions

View File

@ -9,41 +9,26 @@ import Foundation
enum Base32 { enum Base32 {
static let crockfordsEncodingTable: [Character] = Array("0123456789ABCDEFGHJKMNPQRSTVWXYZ") static let crockfordsEncodingTable: [UInt8] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".utf8.map({ $0 })
static let crockfordsDecodingTable: [Character: UInt8] = [ static let crockfordsDecodingTable: [UInt8] = [
"0": 0x00, "O": 0x00, "o": 0x00, // 0 1 2 3 4 5 6 7 8 9 a b c d e f
"1": 0x01, "I": 0x01, "i": 0x01, "L": 0x01, "l": 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0
"2": 0x02, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 1
"3": 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 2
"4": 0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 3
"5": 0x05, 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x01, 0x12, 0x13, 0x01, 0x14, 0x15, 0x00, // 4
"6": 0x06, 0x16, 0x17, 0x18, 0x19, 0x1a, 0xff, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, // 5
"7": 0x07, 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x01, 0x12, 0x13, 0x01, 0x14, 0x15, 0x00, // 6
"8": 0x08, 0x16, 0x17, 0x18, 0x19, 0x1a, 0xff, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, // 7
"9": 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 8
"A": 0x0a, "a": 0x0a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 9
"B": 0x0b, "b": 0x0b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // a
"C": 0x0c, "c": 0x0c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // b
"D": 0x0d, "d": 0x0d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // c
"E": 0x0e, "e": 0x0e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // d
"F": 0x0f, "f": 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // e
"G": 0x10, "g": 0x10, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff // f
"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
] ]
} }
@ -55,37 +40,80 @@ enum Base32Error: Error {
extension Data { extension Data {
/// Decode Crockford's Base32 /// Decode Crockford's Base32
init?(base32Encoded base32String: String, using table: [Character: UInt8] = Base32.crockfordsDecodingTable) { init?(base32Encoded base32String: String, using table: [UInt8] = Base32.crockfordsDecodingTable) {
var str: [Character] = Array(base32String) guard base32String.unicodeScalars.allSatisfy({ $0.isASCII }) else {
while let last = str.last, last == "=" { return nil
str.removeLast()
} }
let div = str.count / 8 var base32String = base32String
let mod = str.count % 8 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 { var buffer = Data(count: dstlen)
func unwrap(_ value: UInt8?) throws -> UInt8 {
guard let value = value else { throw Base32Error.invalidCharacter } let success: Bool = buffer.withUnsafeMutableBytes { (dst: UnsafeMutablePointer<UInt8>) in
return value base32String.withCString(encodedAs: Unicode.ASCII.self) { (src) in
func _strlen(_ str: UnsafePointer<UInt8>) -> 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<UInt8>.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 { guard success else {
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 {
return nil return nil
} }
@ -93,36 +121,89 @@ extension Data {
} }
/// Encode Crockford's Base32 /// Encode Crockford's Base32
func base32EncodedString(using table: [Character] = Base32.crockfordsEncodingTable) -> String { func base32EncodedString(padding: Bool = true, using table: [UInt8] = Base32.crockfordsEncodingTable) -> String {
let div = self.count / 5 var srcleft = self.count
let mod = self.count % 5
return self.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) in let dstlen: Int
var str = [Character]() if padding {
var pad = 0 dstlen = (self.count + 4) / 5 * 8
} else {
dstlen = (self.count * 8 + 4) / 5
}
var dstleft = dstlen
for i in 0 ... div { return self.withUnsafeBytes { (src: UnsafePointer<UInt8>) in
if i == div, mod == 0 { break } var srcp = src
let offset = 5 * i
str.append(table[Int((bytes[offset + 0] >> 3))]) let dst = UnsafeMutablePointer<UInt8>.allocate(capacity: dstlen + 1)
str.append(table[Int((bytes[offset + 0] & 0b00000111) << 2 | bytes[offset + 1] >> 6)]) var dstp = dst
if i == div, mod == 1 { pad = 6; break } defer { dst.deallocate() }
str.append(table[Int((bytes[offset + 1] & 0b00111110) >> 1)])
str.append(table[Int((bytes[offset + 1] & 0b00000001) << 4 | bytes[offset + 2] >> 4)]) let work = UnsafeMutablePointer<UInt8>.allocate(capacity: 8)
if i == div, mod == 2 { pad = 4; break } defer { work.deallocate() }
str.append(table[Int((bytes[offset + 2] & 0b00001111) << 1 | bytes[offset + 3] >> 7)])
if i == div, mod == 3 { pad = 3; break } while srcleft > 0 {
str.append(table[Int((bytes[offset + 3] & 0b01111100) >> 2)]) switch srcleft {
str.append(table[Int((bytes[offset + 3] & 0b00000011) << 3 | bytes[offset + 4] >> 5)]) case _ where 5 <= srcleft:
if i == div, mod == 4 { pad = 1; break } work[7] = srcp[4]
str.append(table[Int((bytes[offset + 4] & 0b00011111))]) 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 { dst[dstlen] = 0
str.append("=") return String(cString: dst)
}
return String(str)
} }
} }

View File

@ -10,6 +10,9 @@ import XCTest
final class Base32Tests: XCTestCase { final class Base32Tests: XCTestCase {
// MARK: -
// MARK: Encode
func testEncodeBase32() { func testEncodeBase32() {
let expected = "00000001D0YX86C6ZTSZJNXFHDQMCYBQ" let expected = "00000001D0YX86C6ZTSZJNXFHDQMCYBQ"
@ -22,6 +25,117 @@ final class Base32Tests: XCTestCase {
XCTAssertEqual(expected, data.base32EncodedString()) 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() { func testDecodeBase32() {
let expected: [UInt8] = [ let expected: [UInt8] = [
0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x3D, 0xD4, 0x19, 0x86, 0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x3D, 0xD4, 0x19, 0x86,
@ -35,6 +149,52 @@ final class Base32Tests: XCTestCase {
XCTAssertEqual(expected, Array(data!)) 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 = [ static var allTests = [
("testEncodeBase32", testEncodeBase32), ("testEncodeBase32", testEncodeBase32),
("testDecodeBase32", testDecodeBase32) ("testDecodeBase32", testDecodeBase32)