//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2023-2024 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// private enum Base64Error: Error { case invalidElementCount case cannotDecode } @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension Data { // MARK: - Init from base64 /// Initialize a `Data` from a Base-64 encoded String using the given options. /// /// Returns nil when the input is not recognized as valid Base-64. /// - parameter base64String: The string to parse. /// - parameter options: Encoding options. Default value is `[]`. public init?(base64Encoded base64String: __shared String, options: Base64DecodingOptions = []) { #if FOUNDATION_FRAMEWORK if let d = NSData(base64Encoded: base64String, options: NSData.Base64DecodingOptions(rawValue: options.rawValue)) { self.init(referencing: d) } else { return nil } #else var encoded = base64String let decoded = encoded.withUTF8 { // String won't pass an empty buffer with a `nil` `baseAddress`. Data(decodingBase64: BufferView(unsafeBufferPointer: $0)!, options: options) } guard let decoded else { return nil } self = decoded #endif } /// Initialize a `Data` from a Base-64, UTF-8 encoded `Data`. /// /// Returns nil when the input is not recognized as valid Base-64. /// /// - parameter base64Data: Base-64, UTF-8 encoded input data. /// - parameter options: Decoding options. Default value is `[]`. public init?(base64Encoded base64Data: __shared Data, options: Base64DecodingOptions = []) { #if FOUNDATION_FRAMEWORK if let d = NSData(base64Encoded: base64Data, options: NSData.Base64DecodingOptions(rawValue: options.rawValue)) { self.init(referencing: d) } else { return nil } #else let decoded = base64Data.withBufferView { Data(decodingBase64: $0, options: options) } guard let decoded else { return nil } self = decoded #endif } init?(decodingBase64 bytes: borrowing BufferView, options: Base64DecodingOptions = []) { guard bytes.count.isMultiple(of: 4) || options.contains(.ignoreUnknownCharacters) else { return nil } // Every 4 valid ASCII bytes maps to 3 output bytes: (bytes.count * 3)/4 let capacity = (bytes.count * 3) >> 2 // A non-trapping version of the calculation goes like this: // let (q, r) = bytes.count.quotientAndRemainder(dividingBy: 4) // let capacity = (q * 3) + (r==0 ? 0 : r-1) let decoded = try? Data( capacity: capacity, initializingWith: { //FIXME: should work with borrowed `bytes` [bytes = copy bytes] in try Data.base64DecodeBytes(bytes, &$0, options: options) } ) guard let decoded else { return nil } self = decoded } // MARK: - Create base64 /// Returns a Base-64 encoded string. /// /// - parameter options: The options to use for the encoding. Default value is `[]`. /// - returns: The Base-64 encoded string. public func base64EncodedString(options: Base64EncodingOptions = []) -> String { #if FOUNDATION_FRAMEWORK // Previously inlinable return _representation.withInteriorPointerReference { return $0.base64EncodedString( options: NSData.Base64EncodingOptions( rawValue: options.rawValue)) } #else if self.isEmpty { return "" } return self.withBufferView { inputBuffer in let capacity = Self.estimateBase64Size(length: self.count) return String(utf8Capacity: capacity) { Self.base64EncodeBytes(inputBuffer, &$0, options: options) } } #endif } /// Returns a Base-64 encoded `Data`. /// /// - parameter options: The options to use for the encoding. Default value is `[]`. /// - returns: The Base-64 encoded data. public func base64EncodedData(options: Base64EncodingOptions = []) -> Data { #if FOUNDATION_FRAMEWORK // Previously inlinable return _representation.withInteriorPointerReference { return $0.base64EncodedData( options: NSData.Base64EncodingOptions( rawValue: options.rawValue)) } #else let dataLength = self.count if dataLength == 0 { return Data() } return self.withBufferView { inputBuffer in let capacity = Self.estimateBase64Size(length: dataLength) return Data(capacity: capacity) { Self.base64EncodeBytes(inputBuffer, &$0, options: options) } } #endif } // MARK: - Internal Helpers static func estimateBase64Size(length: Int) -> Int { // Worst case allow for 64bytes + \r\n per line 48 input bytes => 66 output bytes return ((length + 47) * 66) / 48 } /** Padding character used when the number of bytes to encode is not divisible by 3 */ static let base64Padding : UInt8 = 61 // = /** This method decodes Base64-encoded data. If the input contains any bytes that are not valid Base64 characters, this will throw a `Base64Error`. - parameter bytes: The Base64 bytes - parameter output: An OutputBuffer to be filled with decoded bytes - parameter options: Options for handling invalid input - throws: When decoding fails */ static func base64DecodeBytes( _ bytes: borrowing BufferView, _ output: inout OutputBuffer, options: Base64DecodingOptions = [] ) throws { guard bytes.count.isMultiple(of: 4) || options.contains(.ignoreUnknownCharacters) else { throw Base64Error.invalidElementCount } // This table maps byte values 0-127, input bytes >127 are always invalid. // Map the ASCII characters "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -> 0...63 // Map '=' (ASCII 61) to 0x40. // All other values map to 0x7f. This allows '=' and invalid bytes to be checked together by testing bit 6 (0x40). let base64Decode: StaticString = """ \u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\ \u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\ \u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{3e}\u{7f}\u{7f}\u{7f}\u{3f}\ \u{34}\u{35}\u{36}\u{37}\u{38}\u{39}\u{3a}\u{3b}\u{3c}\u{3d}\u{7f}\u{7f}\u{7f}\u{40}\u{7f}\u{7f}\ \u{7f}\u{00}\u{01}\u{02}\u{03}\u{04}\u{05}\u{06}\u{07}\u{08}\u{09}\u{0a}\u{0b}\u{0c}\u{0d}\u{0e}\ \u{0f}\u{10}\u{11}\u{12}\u{13}\u{14}\u{15}\u{16}\u{17}\u{18}\u{19}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\ \u{7f}\u{1a}\u{1b}\u{1c}\u{1d}\u{1e}\u{1f}\u{20}\u{21}\u{22}\u{23}\u{24}\u{25}\u{26}\u{27}\u{28}\ \u{29}\u{2a}\u{2b}\u{2c}\u{2d}\u{2e}\u{2f}\u{30}\u{31}\u{32}\u{33}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f} """ assert(base64Decode.isASCII) assert(base64Decode.utf8CodeUnitCount == 128) assert(base64Decode.hasPointerRepresentation) let ignoreUnknown = options.contains(.ignoreUnknownCharacters) var currentByte: UInt8 = 0 var validCharacterCount = 0 var paddingCount = 0 var index = 0 for base64Char in bytes { var value: UInt8 = 0 var invalid = false if base64Char >= base64Decode.utf8CodeUnitCount { invalid = true } else { value = base64Decode.utf8Start[Int(base64Char)] if value & 0x40 == 0x40 { // Input byte is either '=' or an invalid value. if value == 0x7f { invalid = true } else if value == 0x40 { // '=' padding at end of input. paddingCount += 1 continue } } } if invalid { if ignoreUnknown { continue } else { throw Base64Error.cannotDecode } } validCharacterCount += 1 // Padding found in the middle of the sequence is invalid. if paddingCount > 0 { throw Base64Error.cannotDecode } switch index { case 0: currentByte = (value << 2) case 1: currentByte |= (value >> 4) output.appendElement(currentByte) currentByte = (value << 4) case 2: currentByte |= (value >> 2) output.appendElement(currentByte) currentByte = (value << 6) case 3: currentByte |= value output.appendElement(currentByte) index = -1 default: fatalError("Invalid state") } index += 1 } guard (validCharacterCount + paddingCount) % 4 == 0 else { // Invalid character count of valid input characters. throw Base64Error.cannotDecode } } /** This method encodes data in Base64. - parameter dataBuffer: A BufferView of the bytes to encode - parameter options: Options for formatting the result - parameter buffer: The buffer to write the bytes into */ static func base64EncodeBytes( _ dataBuffer: BufferView, _ buffer: inout OutputBuffer, options: Base64EncodingOptions = [] ) { // Use a StaticString for lookup of values 0-63 -> ASCII values let base64Chars = StaticString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") assert(base64Chars.utf8CodeUnitCount == 64) assert(base64Chars.hasPointerRepresentation) assert(base64Chars.isASCII) let base64CharsPtr = base64Chars.utf8Start let lineLength: Int var currentLineCount = 0 let separatorByte1: UInt8 var separatorByte2: UInt8? if options.isEmpty { lineLength = 0 separatorByte1 = 0 } else { if options.contains(.lineLength64Characters) { lineLength = 64 } else if options.contains(.lineLength76Characters) { lineLength = 76 } else { lineLength = 0 } switch (options.contains(.endLineWithCarriageReturn), options.contains(.endLineWithLineFeed)) { case (true, true), (false, false): separatorByte1 = UInt8(ascii: "\r") separatorByte2 = UInt8(ascii: "\n") case (true, false): separatorByte1 = UInt8(ascii: "\r") case (false, true): separatorByte1 = UInt8(ascii: "\n") } } func lookupBase64Value(_ value: UInt16) -> UInt32 { let byte = base64CharsPtr[Int(value & 63)] return UInt32(byte) } // Read three bytes at a time, which convert to 4 ASCII characters, allowing for byte2 and byte3 being nil var inputIndex = 0 var bytesLeft = dataBuffer.count while bytesLeft > 0 { let byte1 = dataBuffer[offset: inputIndex] // outputBytes is a UInt32 to allow 4 bytes to be written out at once. var outputBytes = lookupBase64Value(UInt16(byte1 >> 2)) if bytesLeft > 2 { // This is the main loop converting 3 bytes at a time. let byte2 = dataBuffer[offset: inputIndex + 1] let byte3 = dataBuffer[offset: inputIndex + 2] var value = UInt16(byte1 & 0x3) << 8 value |= UInt16(byte2) let outputByte2 = lookupBase64Value(value >> 4) outputBytes |= (outputByte2 << 8) value = (value << 8) | UInt16(byte3) let outputByte3 = lookupBase64Value(value >> 6) outputBytes |= (outputByte3 << 16) let outputByte4 = lookupBase64Value(value) outputBytes |= (outputByte4 << 24) inputIndex += 3 } else { // This runs once at the end of there were 1 or 2 bytes left, byte1 having already been read. // Read byte2 or 0 if there isnt another byte let byte2 = bytesLeft == 1 ? 0 : dataBuffer[offset: inputIndex + 1] var value = UInt16(byte1 & 0x3) << 8 value |= UInt16(byte2) let outputByte2 = lookupBase64Value(value >> 4) outputBytes |= (outputByte2 << 8) let outputByte3 = bytesLeft == 1 ? UInt32(self.base64Padding) : lookupBase64Value(value << 2) outputBytes |= (outputByte3 << 16) outputBytes |= (UInt32(self.base64Padding) << 24) inputIndex += bytesLeft assert(inputIndex == dataBuffer.count) } // The lowest byte of outputBytes needs to be stored at the lowest address, so make sure // the bytes are in the correct order on big endian CPUs. buffer.appendBytes(of: outputBytes.littleEndian, as: UInt32.self) bytesLeft = dataBuffer.count - inputIndex if lineLength != 0 { // Add required EOL markers. currentLineCount += 4 assert(currentLineCount <= lineLength) if currentLineCount == lineLength && bytesLeft > 0 { buffer.appendElement(separatorByte1) if let separatorByte2 { buffer.appendElement(separatorByte2) } currentLineCount = 0 } } } } }