Skip to content

Commit 7a3fbb7

Browse files
committed
support URL-safe Base64 digits
1 parent caee551 commit 7a3fbb7

File tree

5 files changed

+131
-32
lines changed

5 files changed

+131
-32
lines changed

Sources/Base64/Base64.Digits.swift renamed to Sources/Base64/Base64.DefaultDigits.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import BaseDigits
33
extension Base64
44
{
55
public
6-
enum Digits
6+
enum DefaultDigits
77
{
8-
public static
9-
let ascii:[UInt8] =
8+
@usableFromInline
9+
static let ascii:[UInt8] =
1010
[
1111
0x41,
1212
0x42,
@@ -75,10 +75,10 @@ extension Base64
7575
]
7676
}
7777
}
78-
extension Base64.Digits:BaseDigits
78+
extension Base64.DefaultDigits:BaseDigits
7979
{
80-
@inlinable public static
81-
subscript(remainder:UInt8) -> UInt8
80+
@inlinable public
81+
static subscript(remainder:UInt8) -> UInt8
8282
{
8383
Self.ascii[Int.init(remainder & 0b0011_1111)]
8484
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import BaseDigits
2+
3+
extension Base64
4+
{
5+
public
6+
enum SafeDigits
7+
{
8+
@usableFromInline
9+
static let ascii:[UInt8] =
10+
[
11+
0x41,
12+
0x42,
13+
0x43,
14+
0x44,
15+
0x45,
16+
0x46,
17+
0x47,
18+
0x48,
19+
0x49,
20+
0x4a,
21+
0x4b,
22+
0x4c,
23+
0x4d,
24+
0x4e,
25+
0x4f,
26+
0x50,
27+
0x51,
28+
0x52,
29+
0x53,
30+
0x54,
31+
0x55,
32+
0x56,
33+
0x57,
34+
0x58,
35+
0x59,
36+
0x5a,
37+
0x61,
38+
0x62,
39+
0x63,
40+
0x64,
41+
0x65,
42+
0x66,
43+
0x67,
44+
0x68,
45+
0x69,
46+
0x6a,
47+
0x6b,
48+
0x6c,
49+
0x6d,
50+
0x6e,
51+
0x6f,
52+
0x70,
53+
0x71,
54+
0x72,
55+
0x73,
56+
0x74,
57+
0x75,
58+
0x76,
59+
0x77,
60+
0x78,
61+
0x79,
62+
0x7a,
63+
0x30,
64+
0x31,
65+
0x32,
66+
0x33,
67+
0x34,
68+
0x35,
69+
0x36,
70+
0x37,
71+
0x38,
72+
0x39,
73+
0x2d,
74+
0x5f,
75+
]
76+
}
77+
}
78+
extension Base64.SafeDigits:BaseDigits
79+
{
80+
@inlinable public
81+
static subscript(remainder:UInt8) -> UInt8
82+
{
83+
Self.ascii[Int.init(remainder & 0b0011_1111)]
84+
}
85+
}

Sources/Base64/Base64.Values.swift

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
extension Base64
22
{
3-
/// An abstraction over text input, which discards characters that are not
4-
/// valid base-64 digits.
3+
/// An abstraction over text input, which discards characters that are not valid base-64
4+
/// digits. It handles the core Base64 character set, as well as the URL-safe variant.
55
///
6-
/// Iteration over an instance of this type will halt upon encountering the
7-
/// first `'='` padding character, even if the underlying sequence contains
8-
/// more characters.
9-
@frozen public
6+
/// Iteration over an instance of this type will halt upon encountering the first `'='`
7+
/// padding character, even if the underlying sequence contains more characters.
8+
@frozen @usableFromInline
109
struct Values<ASCII> where ASCII:Sequence<UInt8>
1110
{
12-
public
11+
@usableFromInline
1312
var iterator:ASCII.Iterator
1413

15-
@inlinable public
14+
@inlinable
1615
init(_ ascii:ASCII)
1716
{
1817
self.iterator = ascii.makeIterator()
@@ -21,10 +20,10 @@ extension Base64
2120
}
2221
extension Base64.Values:Sequence, IteratorProtocol
2322
{
24-
public
23+
@usableFromInline
2524
typealias Iterator = Self
2625

27-
@inlinable public mutating
26+
@inlinable mutating
2827
func next() -> UInt8?
2928
{
3029
while let digit:UInt8 = self.iterator.next(), digit != 0x3D // '='
@@ -37,9 +36,9 @@ extension Base64.Values:Sequence, IteratorProtocol
3736
return digit - 0x61 + 26
3837
case 0x30 ... 0x39: // 0-9
3938
return digit - 0x30 + 52
40-
case 0x2b: // +
39+
case 0x2b, 0x2d: // +, -
4140
return 62
42-
case 0x2f: // /
41+
case 0x2f, 0x5f: // /, _
4342
return 63
4443
default:
4544
continue

Sources/Base64/Base64.swift

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ enum Base64
1515
/// This function uses the size of the input string to provide a capacity hint
1616
/// for its output, and may over-allocate storage if the input contains many
1717
/// non-digit characters.
18-
@inlinable public static
19-
func decode<Bytes>(_ ascii:some StringProtocol, to _:Bytes.Type = Bytes.self) -> Bytes
18+
@inlinable public
19+
static func decode<Bytes>(_ ascii:some StringProtocol,
20+
to _:Bytes.Type = Bytes.self) -> Bytes
2021
where Bytes:RangeReplaceableCollection<UInt8>
2122
{
2223
self.decode(ascii.utf8, to: Bytes.self)
@@ -32,8 +33,9 @@ enum Base64
3233
/// This function uses the size of the input string to provide a capacity hint
3334
/// for its output, and may over-allocate storage if the input contains many
3435
/// non-digit characters.
35-
@inlinable public static
36-
func decode<ASCII, Bytes>(_ ascii:ASCII, to _:Bytes.Type = Bytes.self) -> Bytes
36+
@inlinable public
37+
static func decode<ASCII, Bytes>(_ ascii:ASCII,
38+
to _:Bytes.Type = Bytes.self) -> Bytes
3739
where Bytes:RangeReplaceableCollection<UInt8>, ASCII:Sequence<UInt8>
3840
{
3941
// https://en.wikipedia.org/wiki/Base64
@@ -65,23 +67,27 @@ enum Base64
6567
}
6668

6769
/// Encodes a sequence of bytes to a base-64 string with padding if needed.
68-
@inlinable public static
69-
func encode<Bytes>(_ bytes:Bytes) -> String where Bytes:Sequence<UInt8>
70+
@inlinable public
71+
static func encode<Bytes>(_ bytes:Bytes) -> String where Bytes:Sequence<UInt8>
7072
{
71-
self.encode(bytes, padding: true)
73+
self.encode(bytes, padding: true, with: DefaultDigits.self)
7274
}
7375

7476
/// Encodes a sequence of bytes to a base-64 string, padding the output with `=` characters
7577
/// if `padding` is true.
76-
@inlinable public static
77-
func encode<Bytes>(_ bytes:Bytes, padding:Bool) -> String where Bytes:Sequence<UInt8>
78+
///
79+
/// The main use-case is `padding: false` with ``SafeDigits``.
80+
@inlinable public
81+
static func encode<Bytes, Digits>(_ bytes:Bytes,
82+
padding:Bool,
83+
with _:Digits.Type) -> String where Bytes:Sequence<UInt8>, Digits:BaseDigits
7884
{
7985
var encoded:String = ""
8086
encoded.reserveCapacity(bytes.underestimatedCount * 4 / 3)
8187
var bytes:Bytes.Iterator = bytes.makeIterator()
82-
while let first:UInt8 = bytes.next()
88+
while let first:UInt8 = bytes.next()
8389
{
84-
encoded.append( Digits[first >> 2])
90+
encoded.append(Digits[first >> 2])
8591

8692
guard let second:UInt8 = bytes.next()
8793
else
@@ -97,7 +103,7 @@ enum Base64
97103
break
98104
}
99105

100-
encoded.append( Digits[first << 4 | second >> 4])
106+
encoded.append(Digits[first << 4 | second >> 4])
101107

102108
guard let third:UInt8 = bytes.next()
103109
else
@@ -111,8 +117,8 @@ enum Base64
111117
break
112118
}
113119

114-
encoded.append( Digits[second << 2 | third >> 6])
115-
encoded.append( Digits[third])
120+
encoded.append(Digits[second << 2 | third >> 6])
121+
encoded.append(Digits[third])
116122
}
117123
return encoded
118124
}

Sources/Base64Tests/Main.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,14 @@ enum Main:TestMain, TestBattery
145145
}
146146
}
147147
}
148+
149+
if let tests:TestGroup = tests / "URL"
150+
{
151+
let encoded:String = Base64.encode("<<???>>".utf8,
152+
padding: false,
153+
with: Base64.SafeDigits.self)
154+
155+
tests.expect(encoded ..? "PDw_Pz8-Pg")
156+
}
148157
}
149158
}

0 commit comments

Comments
 (0)