Skip to content

Commit b7f9625

Browse files
authored
Merge pull request #6 from vapor/encodings
encodings
2 parents 351e835 + ecf50cf commit b7f9625

File tree

5 files changed

+445
-15
lines changed

5 files changed

+445
-15
lines changed

Sources/Bits/Base64Encoder.swift

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/// Encodes and decodes bytes using the
2+
/// Base64 encoding
3+
///
4+
/// https://en.wikipedia.org/wiki/Base64
5+
public final class Base64Encoder {
6+
7+
/// Static shared instance
8+
public static let shared = Base64Encoder.regular
9+
10+
/// Standard Base64Encoder
11+
public static var regular: Base64Encoder {
12+
return Base64Encoder()
13+
}
14+
15+
// Base64URLEncoder
16+
// - note: uses hyphens and underscores
17+
// in place of plus and forwardSlash
18+
public static var url: Base64Encoder {
19+
let encodeMap: Base64Encoder.ByteMap = { byte in
20+
switch byte {
21+
case 62:
22+
return .hyphen
23+
case 63:
24+
return .underscore
25+
default:
26+
return nil
27+
}
28+
}
29+
30+
return Base64Encoder(
31+
padding: nil,
32+
encodeMap: encodeMap
33+
)
34+
}
35+
36+
/// Maps binary format to base64 encoding
37+
static let encodingTable: [Byte: Byte] = [
38+
0: .A, 1: .B, 2: .C, 3: .D,
39+
4: .E, 5: .F, 6: .G, 7: .H,
40+
8: .I, 9: .J, 10: .K, 11: .L,
41+
12: .M, 13: .N, 14: .O, 15: .P,
42+
16: .Q, 17: .R, 18: .S, 19: .T,
43+
20: .U, 21: .V, 22: .W, 23: .X,
44+
24: .Y, 25: .Z, 26: .a, 27: .b,
45+
28: .c, 29: .d, 30: .e, 31: .f,
46+
32: .g, 33: .h, 34: .i, 35: .j,
47+
36: .k, 37: .l, 38: .m, 39: .n,
48+
40: .o, 41: .p, 42: .q, 43: .r,
49+
44: .s, 45: .t, 46: .u, 47: .v,
50+
48: .w, 49: .x, 50: .y, 51: .z,
51+
52: .zero, 53: .one, 54: .two, 55: .three,
52+
56: .four, 57: .five, 58: .six, 59: .seven,
53+
60: .eight, 61: .nine, 62: .plus, 63: .forwardSlash
54+
]
55+
56+
/// Maps base64 encoding into binary format
57+
static let decodingTable: [Byte: Byte] = [
58+
.A: 0, .B: 1, .C: 2, .D: 3,
59+
.E: 4, .F: 5, .G: 6, .H: 7,
60+
.I: 8, .J: 9, .K: 10, .L: 11,
61+
.M: 12, .N: 13, .O: 14, .P: 15,
62+
.Q: 16, .R: 17, .S: 18, .T: 19,
63+
.U: 20, .V: 21, .W: 22, .X: 23,
64+
.Y: 24, .Z: 25, .a: 26, .b: 27,
65+
.c: 28, .d: 29, .e: 30, .f: 31,
66+
.g: 32, .h: 33, .i: 34, .j: 35,
67+
.k: 36, .l: 37, .m: 38, .n: 39,
68+
.o: 40, .p: 41, .q: 42, .r: 43,
69+
.s: 44, .t: 45, .u: 46, .v: 47,
70+
.w: 48, .x: 49, .y: 50, .z: 51,
71+
.zero: 52, .one: 53, .two: 54, .three: 55,
72+
.four: 56, .five: 57, .six: 58, .seven: 59,
73+
.eight: 60, .nine: 61, .plus: 62, .forwardSlash: 63
74+
]
75+
76+
/// Typealias for optionally mapping a byte
77+
public typealias ByteMap = (Byte) -> Byte?
78+
79+
/// Byte to use for padding base64
80+
/// if nil, no padding will be used
81+
public let padding: Byte?
82+
83+
/// If set, bytes returned will have priority
84+
/// over the encoding table. Encoding table
85+
/// will be used as a fallback
86+
public let encodeMap: ByteMap?
87+
88+
/// If set, bytes returned will have priority
89+
/// over the decoding table. Decoding table
90+
/// will be used as a fallback
91+
public let decodeMap: ByteMap?
92+
93+
/// Creates a new Base64 encoder
94+
public init(
95+
padding: Byte? = .equals,
96+
encodeMap: ByteMap? = nil,
97+
decodeMap: ByteMap? = nil
98+
) {
99+
self.padding = padding
100+
self.encodeMap = encodeMap
101+
self.decodeMap = decodeMap
102+
}
103+
104+
/// Encodes bytes into Base64 format
105+
public func encode(_ bytes: Bytes) -> Bytes {
106+
if bytes.count == 0 {
107+
return []
108+
}
109+
110+
let len = bytes.count
111+
var offset: Int = 0
112+
var c1: UInt8
113+
var c2: UInt8
114+
var result: Bytes = []
115+
116+
while offset < len {
117+
c1 = bytes[offset] & 0xff
118+
offset += 1
119+
result.append(encode((c1 >> 2) & 0x3f))
120+
c1 = (c1 & 0x03) << 4
121+
if offset >= len {
122+
result.append(encode(c1 & 0x3f))
123+
if let padding = self.padding {
124+
result.append(padding)
125+
result.append(padding)
126+
}
127+
break
128+
}
129+
130+
c2 = bytes[offset] & 0xff
131+
offset += 1
132+
c1 |= (c2 >> 4) & 0x0f
133+
result.append(encode(c1 & 0x3f))
134+
c1 = (c2 & 0x0f) << 2
135+
if offset >= len {
136+
result.append(encode(c1 & 0x3f))
137+
if let padding = self.padding {
138+
result.append(padding)
139+
}
140+
break
141+
}
142+
143+
c2 = bytes[offset] & 0xff
144+
offset += 1
145+
c1 |= (c2 >> 6) & 0x03
146+
result.append(encode(c1 & 0x3f))
147+
result.append(encode(c2 & 0x3f))
148+
}
149+
150+
return result
151+
}
152+
153+
/// Decodes bytes into binary format
154+
public func decode(_ s: Bytes) -> Bytes {
155+
let maxolen = s.count
156+
157+
var off: Int = 0
158+
var olen: Int = 0
159+
var result = Bytes(repeating: 0, count: maxolen)
160+
161+
var c1: Byte
162+
var c2: Byte
163+
var c3: Byte
164+
var c4: Byte
165+
var o: Byte
166+
167+
while off < s.count - 1 && olen < maxolen {
168+
c1 = decode(s[off])
169+
off += 1
170+
c2 = decode(s[off])
171+
off += 1
172+
if c1 == Byte.max || c2 == Byte.max {
173+
break
174+
}
175+
176+
o = c1 << 2
177+
o |= (c2 & 0x30) >> 4
178+
result[olen] = o
179+
olen += 1
180+
if olen >= maxolen || off >= s.count {
181+
break
182+
}
183+
184+
c3 = decode(s[off])
185+
off += 1
186+
187+
if c3 == Byte.max {
188+
break
189+
}
190+
191+
o = (c2 & 0x0f) << 4
192+
o |= (c3 & 0x3c) >> 2
193+
result[olen] = o
194+
olen += 1
195+
if olen >= maxolen || off >= s.count {
196+
break
197+
}
198+
199+
c4 = decode(s[off])
200+
off += 1
201+
o = (c3 & 0x03) << 6
202+
o |= c4
203+
result[olen] = o
204+
olen += 1
205+
}
206+
207+
return Array(result[0..<olen])
208+
}
209+
210+
// MARK: Private
211+
212+
private func encode(_ x: Byte) -> Byte {
213+
return encodeMap?(x)
214+
?? Base64Encoder.encodingTable[x]
215+
?? Byte.max
216+
}
217+
218+
private func decode(_ x: Byte) -> Byte {
219+
return decodeMap?(x)
220+
?? Base64Encoder.decodingTable[x]
221+
?? Byte.max
222+
}
223+
}

Sources/Bits/Bytes+Base64.swift

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@ import Foundation
22

33
extension Sequence where Iterator.Element == Byte {
44
public var base64Encoded: Bytes {
5-
let bytes = [Byte](self)
6-
let data = Data(bytes: bytes)
7-
8-
let encodedData = data.base64EncodedData()
9-
return encodedData.makeBytes()
5+
let bytes = Array(self)
6+
return Base64Encoder.shared.encode(bytes)
107
}
118

129
public var base64Decoded: Bytes {
13-
let bytes = [Byte](self)
14-
let dataBase64 = Data(bytes: bytes)
15-
guard let dataDecoded = Data(base64Encoded: dataBase64, options: .ignoreUnknownCharacters) else {
16-
return []
17-
}
18-
return dataDecoded.makeBytes()
10+
let bytes = Array(self)
11+
return Base64Encoder.shared.decode(bytes)
12+
}
13+
}
14+
15+
extension Sequence where Iterator.Element == Byte {
16+
public var base64URLEncoded: Bytes {
17+
let bytes = Array(self)
18+
return Base64Encoder.url.encode(bytes)
19+
}
20+
21+
public var base64URLDecoded: Bytes {
22+
let bytes = Array(self)
23+
return Base64Encoder.url.decode(bytes)
1924
}
2025
}

Sources/Bits/Bytes+Hex.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
extension Sequence where Iterator.Element == Byte {
4+
public var hexEncoded: Bytes {
5+
let bytes = Array(self)
6+
return HexEncoder.shared.encode(bytes)
7+
}
8+
9+
public var hexDecoded: Bytes {
10+
let bytes = Array(self)
11+
return HexEncoder.shared.decode(bytes)
12+
}
13+
}

Sources/Bits/HexEncoder.swift

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/// Encodes and decodes bytes using the
2+
/// Hexadeicmal encoding
3+
///
4+
/// https://en.wikipedia.org/wiki/Hexadecimal
5+
public final class HexEncoder {
6+
/// Maps binary format to hex encoding
7+
static let encodingTable: [Byte: Byte] = [
8+
0: .zero, 1: .one, 2: .two, 3: .three,
9+
4: .four, 5: .five, 6: .six, 7: .seven,
10+
8: .eight, 9: .nine, 10: .a, 11: .b,
11+
12: .c, 13: .d, 14: .e, 15: .f
12+
]
13+
14+
/// Maps hex encoding to binary format
15+
/// - note: Supports upper and lowercase
16+
static let decodingTable: [Byte: Byte] = [
17+
.zero: 0, .one: 1, .two: 2, .three: 3,
18+
.four: 4, .five: 5, .six: 6, .seven: 7,
19+
.eight: 8, .nine: 9, .a: 10, .b: 11,
20+
.c: 12, .d: 13, .e: 14, .f: 15,
21+
.A: 10, .B: 11, .C: 12, .D: 13,
22+
.E: 14, .F: 15
23+
]
24+
25+
/// Static shared instance
26+
public static let shared = HexEncoder()
27+
28+
/// When true, the encoder will discard
29+
/// any unknown characters while decoding.
30+
/// When false, undecodable characters will
31+
/// cause an early return.
32+
public let ignoreUndecodableCharacters: Bool
33+
34+
/// Creates a new Hexadecimal encoder
35+
public init(ignoreUndecodableCharacters: Bool = true) {
36+
self.ignoreUndecodableCharacters = ignoreUndecodableCharacters
37+
}
38+
39+
/// Encodes bytes into Hexademical format
40+
public func encode(_ message: Bytes) -> Bytes {
41+
var encoded: Bytes = []
42+
43+
for byte in message {
44+
// move the top half of the byte down
45+
// 0x12345678 becomes 0x00001234
46+
let upper = byte >> 4
47+
48+
// zero out the top half of the byte
49+
// 0x12345678 becomes 0x00005678
50+
let lower = byte & 0xF
51+
52+
// encode the 4-bit numbers
53+
// using the 0-f encoding (2^4=16)
54+
encoded.append(encode(upper))
55+
encoded.append(encode(lower))
56+
}
57+
58+
return encoded
59+
}
60+
61+
/// Decodes hexadecimally encoded bytes into
62+
/// binary format
63+
public func decode(_ message: Bytes) -> Bytes {
64+
var decoded: Bytes = []
65+
66+
// create an iterator to easily
67+
// fetch two at a time
68+
var i = message.makeIterator()
69+
70+
// take bytes two at a time
71+
while let c1 = i.next(), let c2 = i.next() {
72+
// decode the first character from
73+
// letter representation to 4-bit number
74+
// e.g, "1" becomes 0x00000001
75+
let upper = decode(c1)
76+
guard upper != Byte.max || ignoreUndecodableCharacters else {
77+
return decoded
78+
}
79+
80+
// decode the second character from
81+
// letter representation to a 4-bit number
82+
let lower = decode(c2)
83+
guard lower != Byte.max || ignoreUndecodableCharacters else {
84+
return decoded
85+
}
86+
87+
// combine the two 4-bit numbers back
88+
// into the original byte, shifting
89+
// the first back up to its 8-bit position
90+
//
91+
// 0x00001234 << 4 | 0x00005678
92+
// becomes:
93+
// 0x12345678
94+
let byte = upper << 4 | lower
95+
96+
decoded.append(byte)
97+
}
98+
99+
return decoded
100+
}
101+
102+
// MARK: Private
103+
104+
private func encode(_ byte: Byte) -> Byte {
105+
return HexEncoder.encodingTable[byte] ?? Byte.max
106+
}
107+
108+
private func decode(_ byte: Byte) -> Byte {
109+
return HexEncoder.decodingTable[byte] ?? Byte.max
110+
}
111+
}

0 commit comments

Comments
 (0)