Skip to content

Commit

Permalink
support URL-safe Base64 digits
Browse files Browse the repository at this point in the history
  • Loading branch information
tayloraswift committed Jul 1, 2024
1 parent caee551 commit 7a3fbb7
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import BaseDigits
extension Base64
{
public
enum Digits
enum DefaultDigits
{
public static
let ascii:[UInt8] =
@usableFromInline
static let ascii:[UInt8] =
[
0x41,
0x42,
Expand Down Expand Up @@ -75,10 +75,10 @@ extension Base64
]
}
}
extension Base64.Digits:BaseDigits
extension Base64.DefaultDigits:BaseDigits
{
@inlinable public static
subscript(remainder:UInt8) -> UInt8
@inlinable public
static subscript(remainder:UInt8) -> UInt8
{
Self.ascii[Int.init(remainder & 0b0011_1111)]
}
Expand Down
85 changes: 85 additions & 0 deletions Sources/Base64/Base64.SafeDigits.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import BaseDigits

extension Base64
{
public
enum SafeDigits
{
@usableFromInline
static let ascii:[UInt8] =
[
0x41,
0x42,
0x43,
0x44,
0x45,
0x46,
0x47,
0x48,
0x49,
0x4a,
0x4b,
0x4c,
0x4d,
0x4e,
0x4f,
0x50,
0x51,
0x52,
0x53,
0x54,
0x55,
0x56,
0x57,
0x58,
0x59,
0x5a,
0x61,
0x62,
0x63,
0x64,
0x65,
0x66,
0x67,
0x68,
0x69,
0x6a,
0x6b,
0x6c,
0x6d,
0x6e,
0x6f,
0x70,
0x71,
0x72,
0x73,
0x74,
0x75,
0x76,
0x77,
0x78,
0x79,
0x7a,
0x30,
0x31,
0x32,
0x33,
0x34,
0x35,
0x36,
0x37,
0x38,
0x39,
0x2d,
0x5f,
]
}
}
extension Base64.SafeDigits:BaseDigits
{
@inlinable public
static subscript(remainder:UInt8) -> UInt8
{
Self.ascii[Int.init(remainder & 0b0011_1111)]
}
}
23 changes: 11 additions & 12 deletions Sources/Base64/Base64.Values.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
extension Base64
{
/// An abstraction over text input, which discards characters that are not
/// valid base-64 digits.
/// An abstraction over text input, which discards characters that are not valid base-64
/// digits. It handles the core Base64 character set, as well as the URL-safe variant.
///
/// Iteration over an instance of this type will halt upon encountering the
/// first `'='` padding character, even if the underlying sequence contains
/// more characters.
@frozen public
/// Iteration over an instance of this type will halt upon encountering the first `'='`
/// padding character, even if the underlying sequence contains more characters.
@frozen @usableFromInline
struct Values<ASCII> where ASCII:Sequence<UInt8>
{
public
@usableFromInline
var iterator:ASCII.Iterator

@inlinable public
@inlinable
init(_ ascii:ASCII)
{
self.iterator = ascii.makeIterator()
Expand All @@ -21,10 +20,10 @@ extension Base64
}
extension Base64.Values:Sequence, IteratorProtocol
{
public
@usableFromInline
typealias Iterator = Self

@inlinable public mutating
@inlinable mutating
func next() -> UInt8?
{
while let digit:UInt8 = self.iterator.next(), digit != 0x3D // '='
Expand All @@ -37,9 +36,9 @@ extension Base64.Values:Sequence, IteratorProtocol
return digit - 0x61 + 26
case 0x30 ... 0x39: // 0-9
return digit - 0x30 + 52
case 0x2b: // +
case 0x2b, 0x2d: // +, -
return 62
case 0x2f: // /
case 0x2f, 0x5f: // /, _
return 63
default:
continue
Expand Down
34 changes: 20 additions & 14 deletions Sources/Base64/Base64.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ enum Base64
/// This function uses the size of the input string to provide a capacity hint
/// for its output, and may over-allocate storage if the input contains many
/// non-digit characters.
@inlinable public static
func decode<Bytes>(_ ascii:some StringProtocol, to _:Bytes.Type = Bytes.self) -> Bytes
@inlinable public
static func decode<Bytes>(_ ascii:some StringProtocol,
to _:Bytes.Type = Bytes.self) -> Bytes
where Bytes:RangeReplaceableCollection<UInt8>
{
self.decode(ascii.utf8, to: Bytes.self)
Expand All @@ -32,8 +33,9 @@ enum Base64
/// This function uses the size of the input string to provide a capacity hint
/// for its output, and may over-allocate storage if the input contains many
/// non-digit characters.
@inlinable public static
func decode<ASCII, Bytes>(_ ascii:ASCII, to _:Bytes.Type = Bytes.self) -> Bytes
@inlinable public
static func decode<ASCII, Bytes>(_ ascii:ASCII,
to _:Bytes.Type = Bytes.self) -> Bytes
where Bytes:RangeReplaceableCollection<UInt8>, ASCII:Sequence<UInt8>
{
// https://en.wikipedia.org/wiki/Base64
Expand Down Expand Up @@ -65,23 +67,27 @@ enum Base64
}

/// Encodes a sequence of bytes to a base-64 string with padding if needed.
@inlinable public static
func encode<Bytes>(_ bytes:Bytes) -> String where Bytes:Sequence<UInt8>
@inlinable public
static func encode<Bytes>(_ bytes:Bytes) -> String where Bytes:Sequence<UInt8>
{
self.encode(bytes, padding: true)
self.encode(bytes, padding: true, with: DefaultDigits.self)
}

/// Encodes a sequence of bytes to a base-64 string, padding the output with `=` characters
/// if `padding` is true.
@inlinable public static
func encode<Bytes>(_ bytes:Bytes, padding:Bool) -> String where Bytes:Sequence<UInt8>
///
/// The main use-case is `padding: false` with ``SafeDigits``.
@inlinable public
static func encode<Bytes, Digits>(_ bytes:Bytes,
padding:Bool,
with _:Digits.Type) -> String where Bytes:Sequence<UInt8>, Digits:BaseDigits
{
var encoded:String = ""
encoded.reserveCapacity(bytes.underestimatedCount * 4 / 3)
var bytes:Bytes.Iterator = bytes.makeIterator()
while let first:UInt8 = bytes.next()
while let first:UInt8 = bytes.next()
{
encoded.append( Digits[first >> 2])
encoded.append(Digits[first >> 2])

guard let second:UInt8 = bytes.next()
else
Expand All @@ -97,7 +103,7 @@ enum Base64
break
}

encoded.append( Digits[first << 4 | second >> 4])
encoded.append(Digits[first << 4 | second >> 4])

guard let third:UInt8 = bytes.next()
else
Expand All @@ -111,8 +117,8 @@ enum Base64
break
}

encoded.append( Digits[second << 2 | third >> 6])
encoded.append( Digits[third])
encoded.append(Digits[second << 2 | third >> 6])
encoded.append(Digits[third])
}
return encoded
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/Base64Tests/Main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,14 @@ enum Main:TestMain, TestBattery
}
}
}

if let tests:TestGroup = tests / "URL"
{
let encoded:String = Base64.encode("<<???>>".utf8,
padding: false,
with: Base64.SafeDigits.self)

tests.expect(encoded ..? "PDw_Pz8-Pg")
}
}
}

0 comments on commit 7a3fbb7

Please sign in to comment.