import Foundation /// Decodes `ENCODING=QUOTED-PRINTABLE` values, as found in vCard 2.1 exports. /// /// Older address books encode any non-ASCII character this way, so a contact /// named `José` arrives as `Jos=C3=A9` — which a click-wheel iPod displays /// literally rather than decoding. public enum QuotedPrintable { /// Decodes `=XX` escapes and returns the bytes as text. /// /// `charset` is the value of the vCard `CHARSET` parameter when one was /// present. It is only a hint: whatever it claims, valid UTF-8 is decoded /// as UTF-8, since that is overwhelmingly what modern exports contain even /// when they label themselves otherwise. public static func decode(_ value: String, charset: String? = nil) -> String { var bytes: [UInt8] = [] let source = Array(value.utf8) var index = 0 while index < source.count { if source[index] == UInt8(ascii: "="), index + 2 < source.count, let high = hexDigit(source[index + 1]), let low = hexDigit(source[index + 2]) { bytes.append(high << 4 | low) index += 3 } else if source[index] == UInt8(ascii: "="), index + 1 == source.count - 1 { // A trailing '=' is a soft line break that was never joined. index += 2 } else { bytes.append(source[index]) index += 1 } } return decode(bytes: bytes, charset: charset) } static func decode(bytes: [UInt8], charset: String?) -> String { if let text = String(bytes: bytes, encoding: .utf8) { return text } if let charset, let encoding = encoding(named: charset), let text = String(bytes: bytes, encoding: encoding) { return text } if let text = String(bytes: bytes, encoding: .windowsCP1252) { return text } return String(decoding: bytes, as: UTF8.self) } private static func encoding(named charset: String) -> String.Encoding? { switch charset.uppercased() { case "UTF-8", "UTF8": return .utf8 case "ISO-8859-1", "LATIN1", "ISO8859-1": return .isoLatin1 case "WINDOWS-1252", "CP1252": return .windowsCP1252 default: return nil } } private static func hexDigit(_ byte: UInt8) -> UInt8? { switch byte { case UInt8(ascii: "0")...UInt8(ascii: "9"): return byte - UInt8(ascii: "0") case UInt8(ascii: "A")...UInt8(ascii: "F"): return byte - UInt8(ascii: "A") + 10 case UInt8(ascii: "a")...UInt8(ascii: "f"): return byte - UInt8(ascii: "a") + 10 default: return nil } } }