Skip to content

Commit 11aacae

Browse files
authored
Merge pull request #73 from apple/base64url
Add base64url helper methods for Array/ArraySlice of [UInt8]
2 parents 5b0e9a8 + 826097d commit 11aacae

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed

Sources/TSCBasic/Base64URL.swift

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
9+
import Foundation
10+
11+
12+
extension ArraySlice where Element == UInt8 {
13+
14+
/// String representation of a Base64URL-encoded `ArraySlice<UInt8>`.
15+
public func base64URL() -> String {
16+
return base64URL(prepending: [])
17+
}
18+
19+
public func base64URL(prepending: [UInt8]) -> String {
20+
var offset = 0
21+
var recorded = 0
22+
let base64URLCount = ((count * 4 / 3) + 3) & ~3
23+
var arr = [UInt8](repeating: UInt8(ascii: "="), count: prepending.count + base64URLCount)
24+
self.withUnsafeBytes { from in
25+
arr.withUnsafeMutableBytes { to_ in
26+
var to = Base64URLAppendable(to_.baseAddress!)
27+
to.append(prepending)
28+
while true {
29+
switch self.count - offset {
30+
case let n where n >= 3:
31+
to.add = from[offset] >> 2
32+
to.add = from[offset] << 4 | from[offset+1] >> 4
33+
to.add = from[offset+1] << 2 | from[offset+2] >> 6
34+
to.add = from[offset+2]
35+
offset += 3
36+
recorded += 4
37+
case 2:
38+
to.add = from[offset] >> 2
39+
to.add = from[offset] << 4 | from[offset+1] >> 4
40+
to.add = from[offset+1] << 2
41+
recorded += 4
42+
return
43+
case 1:
44+
to.add = from[offset] >> 2
45+
to.add = from[offset] << 4
46+
recorded += 4
47+
return
48+
case 0:
49+
return
50+
default:
51+
fatalError("can't appear here (left=\(self.count-offset))")
52+
}
53+
}
54+
}
55+
}
56+
assert(prepending.count + recorded == arr.count, "prepending=\(prepending.count)+recorded=\(recorded) != \(arr.count)")
57+
return String(bytes: arr, encoding: .ascii)!
58+
}
59+
60+
fileprivate struct Base64URLAppendable {
61+
private let ptr: UnsafeMutableRawPointer
62+
private var offset_: Int = 0
63+
64+
private static let toBase64Table: [UInt8] = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".utf8)
65+
66+
var offset: Int {
67+
return offset_
68+
}
69+
70+
init(_ ptr: UnsafeMutableRawPointer) {
71+
self.ptr = ptr
72+
}
73+
74+
var add: UInt8 {
75+
get { return 0 }
76+
set {
77+
let value: UInt8 = Base64URLAppendable.toBase64Table[Int(newValue & 0x3f)]
78+
ptr.storeBytes(of: value, toByteOffset: offset_, as: UInt8.self)
79+
offset_ += 1
80+
}
81+
}
82+
83+
mutating func append(_ bytes: [UInt8]) {
84+
bytes.withUnsafeBytes { arg in
85+
let fromptr = arg.baseAddress!
86+
ptr.copyMemory(from: fromptr, byteCount: bytes.count)
87+
}
88+
offset_ += bytes.count
89+
}
90+
}
91+
}
92+
93+
extension Array where Element == UInt8 {
94+
95+
/// String representation of a Base64URL-encoded `[UInt8]`.
96+
public func base64URL() -> String {
97+
return ArraySlice(self).base64URL()
98+
}
99+
100+
/// Base64URL encoding. Returns `nil` if the Base64URL encoding is broken.
101+
public init?(base64URL str: String, prepending: [UInt8] = []) {
102+
guard let array = [UInt8](base64URL: str[str.startIndex...]) else {
103+
return nil
104+
}
105+
self = array
106+
}
107+
108+
public init?(base64URL str: Substring, prepending: [UInt8] = []) {
109+
var memory = [UInt8](prepending)
110+
memory.reserveCapacity(prepending.count + str.count * 3 / 4)
111+
112+
var currentValue: UInt32 = 0
113+
var currentBits = 0
114+
115+
for char in str.unicodeScalars {
116+
guard char.isASCII else {
117+
return nil
118+
}
119+
120+
switch char.value {
121+
case let n where n >= UInt32(UInt8(ascii: "A")) && n <= UInt32(UInt8(ascii: "Z")):
122+
currentValue <<= 6
123+
currentValue |= n - UInt32(UInt8(ascii: "A"))
124+
currentBits += 6
125+
case let n where n >= UInt32(UInt8(ascii: "a")) && n <= UInt32(UInt8(ascii: "z")):
126+
currentValue <<= 6
127+
currentValue |= 26 + (n - UInt32(UInt8(ascii: "a")))
128+
currentBits += 6
129+
case let n where n >= UInt32(UInt8(ascii: "0")) && n <= UInt32(UInt8(ascii: "9")):
130+
currentValue <<= 6
131+
currentValue |= 52 + (n - UInt32(UInt8(ascii: "0")))
132+
currentBits += 6
133+
case UInt32(UInt8(ascii: "-")):
134+
currentValue <<= 6
135+
currentValue |= 62
136+
currentBits += 6
137+
case UInt32(UInt8(ascii: "_")):
138+
currentValue <<= 6
139+
currentValue |= 63
140+
currentBits += 6
141+
case UInt32(UInt8(ascii: "=")):
142+
guard currentValue == 0 else {
143+
return nil
144+
}
145+
currentBits = 0
146+
continue
147+
default:
148+
return nil
149+
}
150+
151+
if currentBits >= 8 {
152+
currentBits -= 8
153+
assert(currentBits < 8)
154+
let byte: UInt8 = UInt8(currentValue >> currentBits)
155+
currentValue &= (1 << currentBits) - 1
156+
memory.append(byte)
157+
}
158+
}
159+
160+
guard currentBits == 0 && currentValue == 0 else {
161+
return nil
162+
}
163+
164+
self = memory
165+
}
166+
167+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
9+
import XCTest
10+
11+
import TSCBasic
12+
13+
14+
class Base64URLTests: XCTestCase {
15+
16+
func testEncode() {
17+
XCTAssertEqual([UInt8]([]).base64URL(), "")
18+
XCTAssertEqual([UInt8]([65]).base64URL(), "QQ==")
19+
XCTAssertEqual([UInt8]([65, 65]).base64URL(), "QUE=")
20+
XCTAssertEqual([UInt8]([65, 65, 65]).base64URL(), "QUFB")
21+
}
22+
23+
func testDecode() {
24+
XCTAssertEqual([UInt8](base64URL: ""), [])
25+
XCTAssertEqual([UInt8](base64URL: "QQ=="), [65])
26+
XCTAssertEqual([UInt8](base64URL: "QUE="), [65, 65])
27+
XCTAssertEqual([UInt8](base64URL: "QUFB"), [65, 65, 65])
28+
XCTAssertEqual([UInt8](base64URL: "dGVzdGluZwo="),
29+
[0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x0a])
30+
}
31+
32+
func testRoundTrip() {
33+
for count in 1...10 {
34+
for _ in 0...100 {
35+
var data = [UInt8](repeating: 0, count: count)
36+
for n in 0..<count {
37+
data[n] = UInt8.random(in: 0...UInt8.max)
38+
}
39+
let encoded = data.base64URL()
40+
let decoded = [UInt8](base64URL: encoded[encoded.startIndex...])
41+
XCTAssertEqual(data, decoded)
42+
}
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)