From 7a3fbb75ec4c88421796faaf0b47f16ffc21b348 Mon Sep 17 00:00:00 2001 From: taylorswift Date: Mon, 1 Jul 2024 23:39:53 +0000 Subject: [PATCH] support URL-safe Base64 digits --- ...igits.swift => Base64.DefaultDigits.swift} | 12 +-- Sources/Base64/Base64.SafeDigits.swift | 85 +++++++++++++++++++ Sources/Base64/Base64.Values.swift | 23 +++-- Sources/Base64/Base64.swift | 34 +++++--- Sources/Base64Tests/Main.swift | 9 ++ 5 files changed, 131 insertions(+), 32 deletions(-) rename Sources/Base64/{Base64.Digits.swift => Base64.DefaultDigits.swift} (87%) create mode 100644 Sources/Base64/Base64.SafeDigits.swift diff --git a/Sources/Base64/Base64.Digits.swift b/Sources/Base64/Base64.DefaultDigits.swift similarity index 87% rename from Sources/Base64/Base64.Digits.swift rename to Sources/Base64/Base64.DefaultDigits.swift index 49e8d24..1798c3a 100644 --- a/Sources/Base64/Base64.Digits.swift +++ b/Sources/Base64/Base64.DefaultDigits.swift @@ -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, @@ -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)] } diff --git a/Sources/Base64/Base64.SafeDigits.swift b/Sources/Base64/Base64.SafeDigits.swift new file mode 100644 index 0000000..c47acbd --- /dev/null +++ b/Sources/Base64/Base64.SafeDigits.swift @@ -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)] + } +} diff --git a/Sources/Base64/Base64.Values.swift b/Sources/Base64/Base64.Values.swift index 72b0aad..9f5cd8a 100644 --- a/Sources/Base64/Base64.Values.swift +++ b/Sources/Base64/Base64.Values.swift @@ -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 where ASCII:Sequence { - public + @usableFromInline var iterator:ASCII.Iterator - @inlinable public + @inlinable init(_ ascii:ASCII) { self.iterator = ascii.makeIterator() @@ -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 // '=' @@ -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 diff --git a/Sources/Base64/Base64.swift b/Sources/Base64/Base64.swift index 39c5ee7..ffee5e1 100644 --- a/Sources/Base64/Base64.swift +++ b/Sources/Base64/Base64.swift @@ -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(_ ascii:some StringProtocol, to _:Bytes.Type = Bytes.self) -> Bytes + @inlinable public + static func decode(_ ascii:some StringProtocol, + to _:Bytes.Type = Bytes.self) -> Bytes where Bytes:RangeReplaceableCollection { self.decode(ascii.utf8, to: Bytes.self) @@ -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:ASCII, to _:Bytes.Type = Bytes.self) -> Bytes + @inlinable public + static func decode(_ ascii:ASCII, + to _:Bytes.Type = Bytes.self) -> Bytes where Bytes:RangeReplaceableCollection, ASCII:Sequence { // https://en.wikipedia.org/wiki/Base64 @@ -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) -> String where Bytes:Sequence + @inlinable public + static func encode(_ bytes:Bytes) -> String where Bytes:Sequence { - 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, padding:Bool) -> String where Bytes:Sequence + /// + /// The main use-case is `padding: false` with ``SafeDigits``. + @inlinable public + static func encode(_ bytes:Bytes, + padding:Bool, + with _:Digits.Type) -> String where Bytes:Sequence, 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 @@ -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 @@ -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 } diff --git a/Sources/Base64Tests/Main.swift b/Sources/Base64Tests/Main.swift index 45063f8..e5ce7e4 100644 --- a/Sources/Base64Tests/Main.swift +++ b/Sources/Base64Tests/Main.swift @@ -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") + } } }