Swift में Base64 डिकोडिंग: एक सम्पूर्ण गाइड
आपकी पाइपलाइन के कहीं न कहीं, डेटा नक़ल बनकर घूम रहा है: एक टोकन जो HTTP हेडर में छुपा है, एक एवेटार जो JSON फ़ील्ड के अंदर छिपा है, एक .b64 फ़ाइल जिसे देखना आपने पिछले हफ़्ते ही वादा किया था, और एक ईमेल एटैचमेंट जो अक्षरों की दीवार बनी आया है। Swift में यह नक़ल उतारना भाषा के सबसे आरामदेह कामों में से एक है: एक फ्रामवर्क, एक इनिशियलाइज़र, और नियम-पुस्तक इतनी छोटी कि वह एक स्टिकी नोट पर भी समा जाती है।
इस साइट का होम पेज फ़ॉर्मेट की कहानी पहले से बता चुका है (64 प्रिंट करने योग्य चर, हर चर में छह बिट्स, और आख़िरी ग्रुप में ज़्यादा से ज़्यादा दो = चरों की पैडिंग), इसलिए हम यहाँ उसे नहीं दोहराएँगे। बस दो बातें जेब में रख लें। पहली बात, base64 बाइट्स को टेक्स्ट के कपड़े पहनाने का तरीका है, लॉक नहीं। दूसरी बात, Swift में हर base64 सफ़र एक ही टाइप, Data, से गुज़रता है, और डिकोडर इसी पर एक फ़ेलेबल इनिशियलाइज़र की शक्ल में रहता है। यही एक तथ्य इस आर्टिकल के बाकी हिस्सों का ढर्रा तय करता है, क्योंकि एक फ़ेलेबल इनिशियलाइज़र उसके बाद लिखी गई हर लाइन का ढंग बदल देता है।
पूरा काम एक ही टाइप पर
Swift base64 हेल्पर्स को एक दर्जन मॉड्यूलों में बिखेरता नहीं है, और न आपको कुछ इंस्टॉल करने देता है। डिकोडर है Foundation में मौजूद Data(base64Encoded:options:), और यह फ्रामवर्क के आरंभिक दिनों से ही प्लेटफ़ॉर्म का हिस्सा है (Apple इस इनिशियलाइज़र को iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 और visionOS 1.0 से सूचीबद्ध करता है; एन्कोडिंग पक्ष की लाइन-लेंथ ऑप्शन्स तो iOS 7.0 तक जाती हैं)। Linux और Windows पर वही Foundation ओपन-सोर्स टूलचेन के साथ आता है, इसलिए नीचे का कोड iPhone ऐप, सर्वर वर्कर, और आपके टर्मिनल में किसी स्क्रिप्ट में बिल्कुल एक जैसा व्यवहार करता है।
एक जुड़वाँ इनिशियलाइज़र भी है, Data(base64Encoded: Data, options:), ऐसे मौके के लिए जब आपका base64 स्ट्रिंग की जगह राव ASCII बाइट्स की शक्ल में आता है। दोनों options नाम का आर्गुमेंट लेते हैं, जो डिफ़ॉल्ट रूप से [] होता है। और दोनों में एक चरित्र-विशेषता साझा है जो किसी भी ऑप्शन से ज़्यादा मायने रखती है: ये फ़ेलेबल हैं।
import Foundation
let packed = "SGVsbG8sIFN3aWZ0IQ=="
if let data = Data(base64Encoded: packed) {
let text = String(data: data, encoding: .utf8)
print(text ?? "not text after all")
} else {
print("that was not base64")
}
// Hello, Swift!
Apple की इस इनिशियलाइज़र की डॉक्यूमेंटेशन ख़ूबसूरती से सीधी-सादी है: यह "इनपुट को वैध Base-64 के रूप में पहचाना न जाए तो nil लौटाता है"। कोई एक्सेप्शन नहीं, कोई फेंका गया एरर नहीं, कोई लॉग-स्पैम नहीं। बस एक शांत nil, और यह ज़िम्मेदारी कि वह आपके यूज़र के लिए क्या मतलब रखता है, उसका फैसला करना। अगर आप Swift के base64 के बारे में एक ही बात याद रखें, तो बस यही रखें: डिकोडर कभी क्रैश नहीं होता और कभी शिकायत नहीं करता। वह बस मना कर देता है।
डिकोडर का फैसला: हाँ और नहीं की टेबल
तो इस डिकोडर के लिए "valid" का मतलब क्या है? पता चलता है कि यह कुछ सख़्त नियमों की छोटी-सी लिस्ट है, और वही लिस्ट "डेमो में चलेगा" और "प्रोडक्शन में जीवित रहेगा" की सीमा है। नीचे की टेबल की हर पंक्ति इस इनिशियलाइज़र की मौजूदा टूलचेन पर असली व्यवहार है, इसलिए आप उन्हें सीधे अपने एरर मैसेज में उद्धृत कर सकते हैं:
| इनपुट | फैसला | वजह |
|---|---|---|
TWFu |
Man |
पूरे चार-चर के ग्रुप को पैडिंग की बिल्कुल ज़रूरत नहीं |
TQ== |
M |
एक बाइट और दो पैड, किताबी मामला |
SGVsbG8h |
Hello! |
आठ चर चार का गुणज हैं, इसलिए कोई पैड ज़रूरी नहीं |
==== |
खाली Data |
खालीपन के ऊपर पैडिंग क़ानूनी है, और यह ज़ीरो बाइट्स में डिकोड होती है |
| खाली स्ट्रिंग | खाली Data |
कुछ भी अंदर नहीं, कुछ भी बाहर नहीं, और optional फिर भी सफल है |
TQ |
nil |
लंबाई दो: चारों का ग्रुप वादा तो हुआ पर कभी नहीं आया |
T |
nil |
एक चर छह बिट्स ले जाता है और एक बाइट को आठ चाहिए |
SGVsbG8hTQ |
nil |
दस चर: आख़िरी ग्रुप अपनी पैडिंग के बिना लटका रह गया |
TQ=== |
nil |
तीन पैड: तीसरे के सामने पैड करने को कुछ नहीं बचा |
TQ==TQ |
nil |
पैडिंग के बाद डेटा - बिल्कुल ठुकरा |
SGVs bG8h |
nil |
एक सादा स्पेस वर्णमाला से बाहर है, और सख़्त मोड में कोई दया नहीं |
SGVsbG8h के साथ एक आख़िरी नई लाइन |
nil |
जिस फ़ाइल के अंत की लाइन ब्रेक आपने अभी पढ़ी, वह शोर की गिनती होती है |
तीन पंक्तियों का दूसरी बार नज़र डालने का हक़ है। ==== वाली पंक्ति का मतलब है कि if let जाँच पार हो जाती है और आपका कोड ज़ीरो बाइट्स लेकर आगे बढ़ जाता है, इसलिए अगर खाली पेलोड आपके ऐप में वैध स्थिति नहीं है, तो डिकोड के तुरंत बाद गिनती जाँच लें। खाली-स्ट्रिंग वाली पंक्ति वही चाल है, बस हल्के मेकअप के साथ। और आख़िरी-नई-लाइन वाली पंक्ति वही सबसे बड़ी वज़ह है कि सुबह पूरे-पूरे एन्कोड हुई base64 फ़ाइल दोपहर में डिकोड करने से इनकार कर दे: रास्ते में किसी ने एक लाइन एंडिंग जोड़ दी, और सख़्त डिकोडर उसे निजी मुश्किल समझ लेता है।
टेबल वह मशहूर नरम कोना नहीं दिखा सकती। TQ== और TS== की तुलना करें: दोनों का डिकोड होकर वही बाइट मिलता है, M, क्योंकि उस आख़िरी चर के सबसे निचले दो बिट्स जाँचे जाने से पहले ही फेंक दिए जाते हैं। बस Tg== लगाइए और बिना किसी हलचल के N मिल जाता है। डिकोडर चरों की पुलिस करता है और आख़िरी बिट्स को जाने देता है। यह ढीलपन बग नहीं है, पर इसका मतलब है कि दो अलग-अलग स्ट्रिंग वही डेटा हो सकती हैं, और यह तब मायने रखने लगता है जब आपका सिस्टम base64 वैल्यूज़ की तुलना करे, डीडुप्लीकेट करे, या कैश करे (इसके बारे में और कुछ सिक्योरिटी सेक्शन में)।
जब इनपुट आपके सोचने से ज़्यादा शोरदार हो
असली दुनिया का base64 कभी-कभी ही एक साफ़-सुथरी लाइन के तौर पर आता है। ईमेल एटैचमेंट्स को 76 चरों पर रैप किया जाता है, हर लाइन के बाद एक कार्रेज रिटर्न और लाइन फ़ीड के साथ, जो आदत 1996 की MIME स्पेसिफिकेशन से विरासत में मिली है, और सर्टिफिकेट फ़ाइलें 64 पर रैप होती हैं। उस शोर को संभालने के लिए डिकोडर के पास बिल्कुल एक ही ऑप्शन है, और वह बड़ा-सा है:
import Foundation
let mimeBody = "SGVs\r\nbG8sIG1h\naWwgbm9pc2Uu"
if let data = Data(base64Encoded: mimeBody, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Hello, mail noise.
.ignoreUnknownCharacters की डॉक्यूमेंटेशन इसे इस तरह बताती है कि यह "अनजान non-Base-64 बाइट्स, लाइन एंडिंग चरों सहित, नज़रअंदाज़ करता है", और इस काम के लिए यह सही टूल है: शोर हट जाता है, वर्णमाला बचती है, और पेलोड सलामत निकलता है। पर इस ऑप्शन का एक अंधा कोण है, और वह वही है जो Swift डेवलपर्स को सबसे ज़्यादा काटता है: यह वर्णमाला से बाहर के हर चर को हटा देता है, base64url का - और _ भी समेत। यह उन्हें + और / में बदलता नहीं है; यह बस उन्हें फेंक देता है। उस हटाने के बाद क्या बचता है, इस पर निर्भर है कि आपको nil मिलता है (जब बचे हुए चर पूरे ग्रुप नहीं बन पाते) या, इससे भी बदतर, ग़लत आँकड़े के बाइट्स के साथ भरोसे भरा जवाब। 12 बाइट्स एन्कोड करने वाली 16-चर की base64url स्ट्रिंग ढीले डिकोडर से 9 अलग बाइट्स के रूप में लौट सकती है, बिना किसी एरर और बिना किसी माफ़ी के।
रखने वाला नियम: .ignoreUnknownCharacters ट्रांसपोर्ट शोर (लाइन ब्रेक्स, कॉपी-पेस्ट से उछले हुए स्पेस) के लिए है, कभी वर्णमाला के अंतर के लिए नहीं। अगर पेलोड base64url हो सकता है, तो पहले खुद चर बदल लीजिए, ठीक उसी तरह जैसे अगला सेक्शन दिखाता है, और डिकोडर को साफ़ स्टैंडर्ड स्ट्रिंग दीजिए।
URL वर्णमाला
RFC 4648 का सेक्शन 5 उसी स्टैंडर्ड वर्णमाला का फौजदी तय करता है जिससे आप अब तक मिल रहे हैं: base64url, जहाँ + बनकर - हो जाता है, / बनकर _, और = पैडिंग आमतौर पर गिरा दी जाती है। वजह वही है जो आपके URLs को इमानदार बनाए रखती है: क्वेरी स्ट्रिंग में + को फ़ॉर्म पार्सिंग स्पेस की तरह पढ़ता है, / पथ-सैपरेटर है, और = कीज़ को वैल्यूज़ से अलग करता है। RFC दोनों के रिश्ते के बारे में सीधा है: URL वैरिएंट "base64 encoding के बराबर नहीं समझी जानी चाहिए"। JWTs, Web Push मैसेजेस, YouTube वीडियो आईडीज़, और अधिकांश मॉडर्न API आइडेंटिफायर्स base64url बोलते हैं, इसलिए उम्मीद रखिए कि आप पहले दिन ही उससे मिल जाओगे।
डिकोडिंग की तरफ़ रेसिपी में दो कदम हैं: वर्णमाला बदलो, फिर पैडिंग पूर कर दो, क्योंकि सख़्त डिकोडर को अब भी चार के गुणज चाहिए।
import Foundation
extension String {
func dataFromBase64URL() -> Data? {
var fixed = self
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
return Data(base64Encoded: fixed)
}
}
let tokenPart = "0S__zMWaTC-iVgJ-"
if let bytes = tokenPart.dataFromBase64URL() {
print(bytes.count) // 12
}
मॉड्यूलो वाली लाइन ही पूरी चतुराई है: base64url पेलोड आमतौर पर बिना पैडिंग के आते हैं, और एक या दो = चर (कभी तीन नहीं) उस चारों के ग्रुप को वापस ला देते हैं जिसे डिकोडर उम्मीद करता है। इस पाँच-लाइन के एक्सटेंशन का कोई न कोई रूप आपको हैरान करने वाली तादाद में Swift कोडबेस में मिलेगा, और सही वजह से। भविष्य में इसके छोटा हो जाने की एक वजह है: नवीनतम Apple SDKs (इस लेख के लिखे जाने पर 26.4 और ऊपर) में एन्कोडर के लिए नेटिव .base64URLAlphabet ऑप्शन जुड़ गया है, जबकि मिलती-जुलती डिकोडिंग ऑप्शन्स अभी भी ओपन-सोर्स Foundation में, बाद की टूलचेन के लिए उपलब्धता मार्कर के पीछे, परिपक्व हो रहे हैं। जब तक वह आपके न्यूनतम डिप्लॉयमेंट टारगेट तक नहीं पहुँचता, एक्सटेंशन ही पोर्टेबल जवाब है, और संरचना के बल पर वह हर वर्ज़न पर काम करता रहेगा।
पहले बाइट्स, बाद में शब्द
यह वह फैसला है जो डिकोडर आपकी जगह नहीं कर सकता: वह आपको Data सौंपता है, बाइट्स का एक थैला, और उसे इसका भी अंदाज़ा नहीं कि मूल पेलोड किस चरसेट में लिखा गया था। अगर पेलोड टेक्स्ट था, तो उस चरसेट का चुनाव आपका काम है, और Swift बाइट्स की दुनिया से बाहर निकलने के लिए दो दरवाज़े देता है, जिनके स्वभाव बिल्कुल अलग-अलग हैं।
String(data:encoding:)सख़्त दरवाज़ा है। यह optional लौटाता है, और जब बाइट्स उस एनकोडिंग में वैध न हों जिसका नाम आपने लिया, तो जवाबnilहोता है। वैलिडेशन के लिए आदर्श; खतरनाक अगर आप जवाब को फोर्स-अनरैप कर दें।String(decoding:as:)मना न करने वाला दरवाज़ा है। यह हमेशा एक स्ट्रिंग लौटाता है, और जिस चीज़ की समझ न हो सके, उसके बजाय U+FFFD रिप्लेसमेंट चर जमा देता है। लॉगिंग और प्रीव्यूज़ के लिए आदर्श; खतरनाक अगर आप नतीजे को स्टोर करके उसे डेटा कहने लगें।
import Foundation
let bytes = Data([0xC3, 0xA5]) // a-के-ऊपर-रिंग वाला अक्षर, उसका UTF-8 रूप
print(String(data: bytes, encoding: .utf8) ?? "?") // a-के-ऊपर-रिंग वाला अक्षर, सही पढ़ा
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // दो उलझे हुए अक्षर, वही बाइट्स
print(String(decoding: bytes, as: UTF8.self)) // a-के-ऊपर-रिंग वाला अक्षर, और यह कभी क्रैश नहीं करता
वह रेसिपी जो लगभग सब कुछ कवर करती है: पहले सख़्त UTF-8 आज़माइए, क्योंकि मॉडर्न APIs लगभग हमेशा बस यही मतलब रखते हैं; ISO Latin-1 पर तभी फ़ॉलबैक कीजिए जब कॉन्ट्रैक्ट चुप हो और आपको पढ़ने लायक-पर-ग़लत चुपचाप बरतने से बेहतर लगे; और मना न करने वाला दरवाज़ा केवल डीबग आउटपुट के लिए आरक्षित रखिए। और एक अदृश्य घुसपैठिया जिसके लिए जाँच करनी चाहिए: अगर पेलोड UTF-8 BOM से शुरू हो (तीन बाइट्स EF BB BF), तो सख़्त कन्वर्ज़न उसे वहीं रख लेता है, और आपकी स्ट्रिंग अब एक अदृश्य U+FEFF चर से शुरू होती है जो चुपचाप इक्वालिटी चेक्स और JSON राउंड-ट्रिप्स तोड़ देता है। जब स्पेसिफिकेशन में उसका वादा न हो, तो प्रीफ़िक्स चेक से उसे हटा दीजिए।
फ़ाइलें खोलना
वह काम, "यहाँ एक .b64 फ़ाइल है, मुझे दिखाओ वह क्या छुपाती है", है पढ़ो, सपाट करो, डिकोड करो, और लिखो। ट्रिम सजावट नहीं है; यह उस फ़ाइल का फ़र्क़ है जो खुलती है और उस फ़ाइल का जो nil लौटाती है, क्योंकि टूल्स, मेल क्लाइंट्स, और एडिटर सब अंत में लाइन ब्रेक छोड़ने के दीवाने हैं:
import Foundation
let inbox = URL(fileURLWithPath: "Downloads/avatar.b64")
let outbox = URL(fileURLWithPath: "Downloads/avatar.png")
let raw = try String(contentsOf: inbox, encoding: .utf8)
if let data = Data(base64Encoded:
raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
try data.write(to: outbox)
} else {
print("the file was not base64 after all")
}
अगर फ़ाइल MIME-wrapped है (हर 76 चरों पर लाइन ब्रेक), तो आपके पास दो साफ़ रास्ते हैं: .ignoreUnknownCharacters के साथ डिकोड कीजिए और ऑप्शन को लाइन एंडिंग्स खुआने दीजिए, या सख़्त डिकोड से पहले उन्हें खुद replacingOccurrences से हटा लीजिए। दोनों एक-एक लाइन के काम हैं। बस बड़ी फ़ाइलों के लिए, पूरा पढ़ने की बजाय अलाइन्ड ग्रुप्स में डिकोड कीजिए: हर चार-चर का ग्रुप अपने-आप डिकोड होता है, इसलिए आप रीड बाउंडरीज़ के पार केवल मौजूदा ग्रुप और एक छोटा-सा शेष ले जा सकते हैं।
import Foundation
func decodeBase64Chunks(_ stream: InputStream, into result: inout Data) throws {
let chunkSize = 65_536
var buffer = [UInt8](repeating: 0, count: chunkSize)
var leftover = ""
result = Data()
stream.open()
defer { stream.close() }
while stream.hasBytesAvailable {
let read = stream.read(&buffer, maxLength: chunkSize)
if read < 0 { throw CocoaError(.fileReadUnknown) }
if read == 0 { break }
var text = String(decoding: buffer[0..<read], as: UTF8.self)
text = text.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\n", with: "")
text = leftover + text
if text.count % 4 != 0 {
let whole = text.count - (text.count % 4)
leftover = String(text.suffix(text.count - whole))
text = String(text.prefix(whole))
} else {
leftover = ""
}
guard !text.isEmpty else { continue }
guard let part = Data(base64Encoded: text) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
if !leftover.isEmpty {
guard let part = Data(base64Encoded: leftover) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
}
फ़ाइल कितनी भी बड़ी हो, मेमोरी सपाट रहती है: एक रीड बफ़र, एक बचा हुआ टुकड़ा, और वह नतीजा जिसे आप बना रहे हैं। वही लूप वह डाउनलोड संभालता है जो वायर पर base64 के रूप में आता है, वह लॉग फ़ाइल जो असल में एन्कोड किया हुआ स्ट्रीम है, या वह कोई भी पेलोड जो आपके हाथ में नहीं समाता।
JWTs: तीन डॉट्स पढ़ना
एक कंपैक्ट JSON Web Token तीन base64url हिस्सों से बनता है जो डॉट्स से जुड़े होते हैं, और उन हिस्सों में से पहले दो सादे JSON हैं, बस ट्रेंच कोट पहने। ये बिना पैडिंग के आते हैं, जो वही कम्बिनेशन है जो सख़्त डिकोडर देखते ही रिजेक्ट कर देता है, इसलिए URL सेक्शन का आपका dataFromBase64URL() हेलपर पूरा भारी काम करता है:
import Foundation
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
func openPart(_ part: String) -> String? {
var fixed = part
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
guard let data = Data(base64Encoded: fixed) else { return nil }
return String(data: data, encoding: .utf8)
}
let pieces = token.split(separator: ".")
print(openPart(String(pieces[0])) ?? "?")
// {"alg":"HS256","typ":"JWT"}
print(openPart(String(pieces[1])) ?? "?")
// {"sub":"1234567890","name":"John Doe"}
दो सलाहें साथ में चली आती हैं। JWT sign किया जाता है, एन्क्रिप्ट नहीं: हेडर और पेलोड दोनों सार्वजनिक जानकारी हैं, और यही वजह है कि पासवर्ड कभी भी उसके अंदर नहीं रखना चाहिए (एन्क्रिप्ट किया हुआ फौजदी, JWE, पूरी तरह अलग स्पेसिफिकेशन है)। और तीसरा डॉट-अलग हिस्सा क्रिप्टोग्राफिक सिग्नेचर है, डॉक्यूमेंट नहीं, इसलिए हिस्सा एक और दो डिकोड कीजिए और बाकी को अपने-आप छोड़ दीजिए।
Data URIs: कॉमा के पीछे की फ़ाइल
Web APIs को data: स्कीम के साथ टेक्स्ट के अंदर बायनेरी छुपाना बहुत पसंद है: प्रोफ़ाइल फ़ील्ड में एक PNG, CSS ब्लॉब में एक फ़ॉन्ट, सेटिंग्स फ़ाइल में एक QR कोड। फ़ॉर्मेट है data:{mime};base64,{payload}, और पेलोड को छीलकर निकालना बस एक स्प्लिट दूर है:
import Foundation
let uri = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
let payload = uri.components(separatedBy: ",").last ?? ""
if let bytes = Data(base64Encoded: payload) {
print(String(decoding: bytes.prefix(6), as: UTF8.self)) // GIF89a
print(bytes.count) // 42
} else {
print("not a base64 data uri")
}
इस उदाहरण में उसी मशहूर 42-बाइट की ट्रांसपेरेंट GIF इस्तेमाल हुई है, फ़ॉर्मेट की सबसे छोटी इमेज, इसलिए इसकी आरंभिक चर इंटरनेट पर लगभग किसी भी base64 स्ट्रिंग से ज़्यादा कोडबेस में नज़र आते हैं। Apple प्लेटफ़ॉर्म्स पर पाइपलाइन एक लाइनर पर ख़त्म होती है: वही Data जिसे आपने अभी डिकोड किया, सीधा UIImage(data:) या NSImage(data:) में चला जाता है, और यही वजह है कि "API से एवेटार दिखाना" एक छोटा फ़ीचर है, एक प्रोजेक्ट नहीं।
HTTP: Basic हेडर और उसके साथी
पुराना Authorization: Basic हेडर एक यूज़रनेम और पासवर्ड है, जो कॉलन से जुड़े हैं, और सफ़र के लिए स्टैंडर्ड base64 से पैक किए गए हैं (URL डायलेक्ट नहीं: यह तो एक हेडर में रहता है, जहाँ + और / बिल्कुल बेख़तर हैं)। इसे खोलना बस एक स्प्लिट और एक डिकोड है:
import Foundation
let header = "Basic ZWRpdG9yOnMzY3JldA=="
let packed = header.replacingOccurrences(of: "Basic ", with: "")
if let creds = Data(base64Encoded: packed) {
print(String(data: creds, encoding: .utf8) ?? "") // editor:s3cret
} else {
print("malformed header")
}
सिक्योरिटी वाली फुटनोट को ज़ोरदार रखिए, क्योंकि वह हर base64 पर लागू होती है जिससे आप कभी भी मिलेंगे: यह पैकिंग है, सुरक्षा नहीं। Basic auth केवल HTTPS पर ही स्वीकार्य है, जहाँ असली पहरेदारी TLS करता है और base64 बस बाइट्स को हेडर ग्रामर न तोड़ने से रोकता है। वही तर्क Authorization: Bearer टोकन्स की भी व्याख्या करता है: टोकन खुद एक JWT है, इसलिए JWT सेक्शन की डिकोडिंग रेसिपी उस पर बिना बदलाव के लागू होती है।
ईमेल: 76-चर की आदत
base64 में एन्कोड किया हुआ ईमेल एटैचमेंट 76 चरों पर रैप होता है, CRLF लाइन एंडिंग्स के साथ, और वह बिल्कुल वही शोर है जिसके लिए ढीला ऑप्शन मौजूद है। राव MIME हेडर्स बताते हैं कि सेंडर ने कौन-सी वर्णमाला और कौन-सी रैपिंग इस्तेमाल की (Content-Transfer-Encoding: base64), और इलाज बस एक फ्लैग है:
import Foundation
let attachment = "VGhpcyBhdHRhY2htZW50IHN1cnZpdmVk\r\nIHRoZSA3Ni1jaGFyYWN0ZXIgaGFiaXQu"
if let data = Data(base64Encoded: attachment, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// This attachment survived the 76-character habit.
अगर आप मेल फ़ीचर पढ़ने वाले की बजाय लिखने वाले हैं, तो याद रखिए कि 76-चर की रैपिंग आपको भी कीमत लगाती है: हर 76 चरों पर लाइन ब्रेक के साथ, एन्कोड किया हुआ टेक्स्ट मूल आकार का करीब 137 प्रतिशत तक पहुँच जाता है, और इसीलिए पुराने मेल इंजीनियर्स एटैचमेंट साइज़ आँखों से अनुमान लगाकर उस शॉर्टकट से आँकते थे: "मूल को 1.37 से गुणा करो, और हेडर्स के लिए लगभग 800 बाइट्स जोड़ दो"। यह आँकड़ा अब फ़ॉलकलोर बन चुका है, पर गणित वही गणित है।
दोहरी-रैप किया हुआ पेलोड
base64 की दुनिया में सबसे आम "मेरा डेटा कोरप्ट है" टिकेट वही डेटा है जो दो बार पैक किया गया: एक इंटीग्रेशन लेयर ने उसे एन्कोड किया, और एक दूसरी लेयर ने, जिसने डॉक्यूमेंटेशन कभी नहीं पढ़ा, उसने नतीजे को एन्कोड कर दिया। बचाव की चाल यह है: एक बार डिकोड कीजिए, देखिए क्या मिला, और अगर नतीजा खुद एक साफ़ base64-सी स्ट्रिंग है (सही लंबाई, सही वर्णमाला, कुछ भी आश्चर्यजनक नहीं), तो जानबूझकर एक बार और डिकोड कीजिए, और रुकिए। ऐसा लूप न लिखें जो तब तक डिकोड करता रहे जब तक वह फेल न हो जाए। ऐसा लूप ख़ुशी-ख़ुशी एक बिल्कुल सही फ़ाइल खा लेता है जिसका कंटेंट बस base64-सा दिखता है, और उस चले जाने के बाद यह पता लगाना मुश्किल हो जाता है कि मूल डेटा कहाँ से शुरू होता था।
import Foundation
func unwrapOnce(_ packed: String) -> Data? {
let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
return Data(base64Encoded: cleaned)
}
let suspicious = "WVdKag==" // पहले से ही पैक दिखता है
if let first = unwrapOnce(suspicious) {
let inner = String(data: first, encoding: .utf8) ?? ""
if let second = unwrapOnce(inner) {
print("it was wrapped twice:", String(data: second, encoding: .utf8) ?? "?")
}
}
// it was wrapped twice: abc
दो अनरैप, दो जानबूझकर लिए फैसले, और एक पेलोड जो आख़िरकार बस abc बनकर वापस आ गया।
nil को मतलब देना
क्योंकि डिकोडर फेंकने की जगह nil जवाब देता है, आपके base64 कोड की एरर-हैंडलिंग शैली एक ऐसा चुनाव है जिसे आप खुद करते हैं, और वह चुनाव जिसके लिए आप बाद में खुद के शुक्रगुज़ार होंगे, वह एक छोटा सा रैपर है जो चुपचाप मना करने को ज़ोरदार, स्पष्ट एरर में बदल देता है:
import Foundation
enum Base64Failure: Error, CustomStringConvertible {
case notBase64(Int)
var description: String {
switch self {
case .notBase64(let length):
return "input of \(length) characters is not valid base64"
}
}
}
func decodeStrict(_ text: String) throws -> Data {
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard let data = Data(base64Encoded: cleaned) else {
throw Base64Failure.notBase64(cleaned.count)
}
return data
}
do {
let bytes = try decodeStrict("c3ludGF4IGVycm")
print(String(data: bytes, encoding: .utf8) ?? "?")
} catch {
print(error) // input of 14 characters is not valid base64
}
रैपर नॉर्मलाइज़ेशन का एकमात्र ठिकाना भी बन जाता है: ट्रिम, कोई भी वर्णमाला का अनुवाद, कोई भी पैडिंग पूर करना। कॉलर्स को एक फ़ंक्शन मिलता है, फ़ेलियर का एक मतलब, और नज़र में कहीं ! नहीं। Data(base64Encoded:)! को फोर्स-अनरैप करना वही रास्ता है जिससे ख़राब पेलोड क्रैश हुए ऐप बन जाता है, और रैपर इसके लिए सस्ता बीमा है। वही पैटर्न कमांड लाइन पर भी काम करता है, जहाँ CommandLine.arguments और FileHandle write वाली स्क्रिप्ट "शेल से इस फ़ाइल को डिकोड करना" को एक पाँच-लाइन की यूटिलिटी बना देती है, किसी वेबसाइट के रास्ते कॉपी-पेस्ट की लंबी राह नहीं।
सिक्योरिटी, बाइट्स में मापी गई
- यह एन्क्रिप्शन नहीं है। Base64 एक रिवर्सिबल, तुरंत पढ़ा जा सकने वाला री-पैकिंग है। अगर आपके थ्रेट मॉडल में एक इंसान है, जिसके पास ब्राउज़र और पाँच सेकंड हैं, तो आपके पास ज़ीरो सुरक्षा है, और हर JWT हेडर यह बात रोज़ साबित करता है।
- तुलना से पहले कैनॉनिकल रूप दो। क्योंकि
TQ==औरTS==का डिकोड होकर वही बाइट्स मिलते हैं, दो सिस्टम वही डेटा दो अलग लिखातों में रख सकते हैं। एक 2022 के पेपर, "अमल में Base64 मैलेबिलिटी", ने दस्तावेज़ी किया कि असली दुनिया में वह टूटी हुई यूनिकनेस गैरंटी क्या करती है: लॉग मिसमैच, डीनियल-ऑफ-सर्विस हमले, और डेटाबेस में दोहरी एंट्रीज़। अगर आपका Swift ऐप base64 वैल्यूज़ को कैश करता है, डीडुप्लीकेट करता है, या तुलना करता है, तो गेट पर एक कैनॉनिकल डिकोड (या एक कैनॉनिकल री-एन्कोड) चलाइए। - डिकोड से पहले इनपुट पर सीमा लगाइए। N चरों को डिकोड करने पर N बाइट्स का लगभग तीन-चौथाई एलोकेट होता है, और इस दौरान आप इनपुट स्ट्रिंग को हाथ में ही पकड़े रहे हैं। एक दुश्मन क्लाइंट अक्षर
Aके 100 मेगाबाइट भेज सकता है और देख सकता है कि डिकोडर के "नहीं" कहने से पहले आपकी मेमोरी कैसे चढ़ती है। पहले लंबाई जाँचिए, सस्ती तरह से, और जो बहुत बड़ा हो उसे रिजेक्ट कीजिए। - ढीले ऑप्शन को फ़िल्टर की तरह इस्तेमाल करने से बचें।
.ignoreUnknownCharactersचर हटा देता है। इससे एक "sanitizing" पास वैध base64url पेलोड को, बिना एरर के, अलग डेटा में बदल सकता है। यह लाइन ब्रेक्स के लिए शोर फ़िल्टर है, वैलिडेटर नहीं। - जहाँ तक हो सके, उसे URL से दूर रखिए। क्वेरी स्ट्रिंग्स या पेथ्स में बड़े base64 पेलोड आरामदायक URL लंबाई पार कर जाते हैं और प्रॉक्सीज़ उनके साथ खिलवाड़ कर देते हैं। उनकी जगह रेक्वेस्ट बॉडीज़, फ़ाइलों, या टोकन्स में रखिए।
परफ़ॉर्मेंस, थोड़ी सी बात
डिकोडर लुकअप-टेबल पर एक चलन है: हर चर एक छोटी टेबल में इंडेक्स होता है, और कुछ बिट्स को शिफ़्ट करके or कर दिया जाता है, आउटपुट बाइट्स में। मौजूदा टूलचेन पर यह काफ़ी तेज़ है, जब तक कि चीज़ मेमोरी में समा जाए, और याद रखने वाला आँकड़ा है आउटपुट अनुपात: डिकोड किए गए बाइट्स इनपुट लंबाई के लगभग तीन-चौथाई होते हैं, इसलिए 4-मेगाबाइट की स्ट्रिंग का ख़र्च है करीब 3-मेगाबाइट का नतीजा, उस स्ट्रिंग के ऊपर जो आप पहले से ही हाथ में पकड़े हैं। अगर आप ऐसे रास्ते पर हैं जहाँ Foundation खुद की इज़ाज़त नहीं है (बहुत गहरा एम्बेडेड टारगेट, WebAssembly बंडल), तो कम्युनिटी पैकेज swift-extras-base64 वह लोकप्रिय विकल्प है: शुद्ध Swift, Foundation डिपेंडेंसी के बिना, base64url और पैडिंग ऑप्शन्स के साथ RFC 4648 कंप्लाइंट एन्कोडर और डिकोडर, और बेंचमार्क्स जो उसे Foundation से कई गुना तेज़ दिखाते हैं। उसी पैकेज की एक पुरानी इम्प्लीमेंटेशन तो swift-nio के WebSocket सपोर्ट के अंदर भी शिप होती है, जो एक साइड प्रोजेक्ट के लिए प्रोडक्शन-ग्रेड की क़रीब-क़रीब जितना हो सके उतना करीब है। एक सादे ऐप या स्क्रिप्ट के लिए यह ज़रूरी नहीं सामान है; Swift के सीमित कोने के लिए यह स्टैंडर्ड जवाब है।
अनपैकिंग का एक दशक
Swift ने इनमें से कुछ भी नहीं बनाया, और यह जानना ज़रूरी है कि टूलबॉक्स का हर हिस्सा कहाँ से आया:
- 1980 के दशक, वही-मशीन का दौर। इस परिवार की सबसे पहली एन्कोडिंग्स (UNIX पर uuencode, TRS-80 और क्लासिक Mac पर BinHex) फ़ाइलें उन मशीनों के बीच ले जाती थीं जो मानती थीं कि दूसरी तरफ़ की मशीन उनके जैसी ही है। uuencode की वर्णमाला में अपेस-कैस अक्षर, अंक, और पंक्तिचिह्न थे, और उसके अक्षर लगातार ASCII पोज़िशन्स पर बैठे हैं, इसलिए एन्कोडिंग में बस 32 जोड़ने की बात थी, कोई लुकअप टेबल बिल्कुल नहीं। उस दौर के डिकोडर्स बहुत कुछ मान सकते थे, और जिस पल डेटा एक इकोसिस्टम से दूसरे में गया, वह पलट गया।
- 1987, वर्णमाला को पता मिला। RFC 989 (Privacy-Enhanced Mail, फ़रवरी 1987) ने 64-चर की वर्णमाला को स्टैंडर्ड बनाया, लाइन्स को बिल्कुल 64 चरों पर रैप किया, और पैडिंग के लिए
=का इस्तेमाल किया और एन्कोड किया हुआ पर एन्क्रिप्ट नहीं डेटा को चिह्नित करने के लिए*। हर PEM-स्टाइल ब्लॉक उसी डॉक्यूमेंट की संतान है। - 1996, ढीला दौर। MIME (RFC 2045) ने वर्णमाला को ईमेल के लिए लिया, रैप को 76 चरों पर ले गया, और कंप्लाइंट डिकोडर्स को यह निर्देश दिया कि वर्णमाला के बाहर के हर चर को नज़रअंदाज़ करें, जैसे CRLF लाइन ब्रेक्स। यही वह दौर है जिसने एक पीढ़ी को मेहरबान डिकोडर्स की उम्मीद की तालीम दी, और वही वह दौर है जिसकी उम्मीदों को Swift का सख़्त डिफ़ॉल्ट जानबूझकर तोड़ता है।
- 2003 से 2006, नियम सख़्त होते हैं। RFC 3548 (2003) ने परिवार को एकजुट करने का पहला प्रयास किया; RFC 4648 (अक्टूबर 2006) ने बात तय की, पैडिंग नियमों को क़ानूनी रूप दिया, और URL-safe वर्णमाला जोड़ी। इसका डिकोडर वाला पैराग्राफ़ वही है जो Swift मानता है: वर्णमाला के बाहर के चरों को रिजेक्ट कीजिए, जब तक कि वह फ़ॉर्मेट जिसका आप ख़िदमत कर रहे हैं, उन्हें नज़रअंदाज़ करने का खुलकर कहे, जैसे MIME करता है।
- 2013 से 2014, API पार्श्व में इंतज़ार में था। Apple की
NSDataक्लास सालों से base64 को पैक और अनपैक कर रही थी, और ऑप्शन्स-आधारित API, जिसका अपना डिकोडिंग ऑप्शन था, 2013 में iOS 7 में उतरा, Swift के अस्तित्व में आने से एक साल पहले। जब 9 सितंबर 2014 को Swift 1.0 शिप हुआ, तो डिकोडर भाषा के साथ अंदर चला आया, और उस दिन से वही चरित्र बनाए हुए है: सख़्त कोर, एक ढीला नॉब, फ़ेलेबल इनिशियलाइज़र। - 3 दिसंबर 2015, Linux को डिकोडर मिला। उसी दिन Swift को ओपन-सोर्स किया गया, और उसके साथ Foundation का base64 Linux पर पहुँचा और, बाद में, Windows पर। "non-Apple मशीन पर Swift में Base64 डिकोडिंग" की उम्र एक दशक भी नहीं हुई: वह देर से आया मेहमान, जिस पार्टी में 1987 में शुरुआत हुई थी।
- 2023 से 2026, फिर से लिखाई। Foundation की रिवाइट (swift-foundation प्रोजेक्ट) ने
Dataको शुद्ध-Swift कोर में स्थानांतरित कर दिया, और 2025 में एक कम्युनिटी प्रस्ताव ने नेटिव base64url और पैडिंग-हटाने वाले ऑप्शन्स जोड़े। इस लेख के लिखे जाने पर नवीनतम SDK बेटा वर्ज़न्स और ओपन-सोर्स टूलचेन एन्कोडिंग ऑप्शन्स के साथ शिप कर रहे हैं, जबकि डिकोडिंग ऑप्शन्स अभी भी ओपन-सोर्स टूलचेन में परिपक्व हो रहे हैं, इसलिए इंतज़ार के समय हाथ से बनाए एक्सटेंशन ही यूनिवर्सल जवाब बने रहे हैं।
छोटे-छोटे आश्चर्य
====क़ानूनी इनपुट है। चार पैड और कोई डेटा नहीं, इनका डिकोड होकर खालीDataबनता है, वह एकमात्र base64 स्ट्रिंग जिसका पूरा कंटेंट "यहाँ कुछ भी नहीं है", और Swift इससे सहमत है।- डिकोडर की चरों की पुलिस बिट्स की पुलिस के काम की जाँच नहीं करती:
TS==औरTQ==दोनों आपकोMसौंपते हैं, जबकिTg==आपकोNसौंपता है। वही ग्रामर, अलग बिट्स, कोई सवाल नहीं। - Swift का
Database64 को डिकोड कर सकता है जो स्ट्रिंग की जगह बाइट्स के रूप में आया हो,Data(base64Encoded: Data)वैरिएंट के रास्ते, इसलिए वह पेलोड जो वायर पर ASCII के रूप में गुज़रा, स्ट्रिंग राउंड-ट्रिप को पूरी तरह छोड़ सकता है। - वह शब्द जो टेस्ट वेक्टर के जन्म से ही base64 है,
foobarहै, और यह पैक होकरZm9vYmFyबनता है। अगर आपने कभी भी असली दुनिया में base64 का उदाहरण देखा है, तो foobar शामिल था, इसकी उम्मीद काफी है। - मशहूर 1x1 ट्रांसपेरेंट GIF बिल्कुल 42 बाइट्स है और मैजिक वर्ड
GIF89aसे शुरू होता है, इसलिए इसके पहले आठ एन्कोड किए गए चर पृथ्वी पर लगभग किसी भी अन्य base64 प्रीफ़िक्स से ज़्यादा कोडबेस में नज़र आते हैं। - आधुनिक ओपन-सोर्स डिकोडर अपने ग़लत-चर चेक को एक ही तुलना से करता है: यह चार हर-पोज़िशन लुकअप वैल्यूज़ को or करके जोड़ता है और नतीजे की तुलना एक सैंटिनल से करता है, इसलिए एक ब्रांच पूरे चार-चर के ग्रुप का भविष्य तय करती है। पुरानी इम्प्लीमेंटेशन ने वही काम 128-बाइट की टेबल से किया जहाँ 0x80 पर या उससे ऊपर कोई भी वैल्यू "अक्षर नहीं" का मतलब रखती थी।
- UTF-8 BOM अदृश्य होते हैं: पेलोड की शुरुआत का
EF BB BFएक U+FEFF चर बन जाता है जो सख़्त कन्वर्ज़न में बचकर निकलता है, और कुछ लाइनों बाद इक्वालिटी चेक्स तोड़ देता है। - Swift उस वर्णमाला से 27 साल छोटा है जिसके डिकोड करता है। भाषा 2014 में शिप हुई; वो 64 अक्षर जिनसे वह निपटता है, 1987 में स्टैंडर्ड बने थे और तब से बदले नहीं हैं।
यही है पूरा डिकोडिंग टूलबॉक्स: एक फ़ेलेबल इनिशियलाइज़र, जिसकी नियम-पुस्तक छोटी है, एक ढीला नॉब, जिसका डॉक्यूमेंटेड अंधा कोण है, पाँच-लाइन का base64url हेलपर, चरसेट का वह फैसला जो आपकी ज़िम्मेदारी है, बड़ी फ़ाइलों के लिए चंक्ड लूप, और एक रैपर जो nil को मतलब देता है। डिकोडिंग वही जगह है जहाँ base64 काटता है, और अब आपको हर दांत का नाम पता है। जब काम उल्टा हो जाता है और आप बाइट्स को अनपैक करने की बजाय सफ़र के लिए पैक करने लगते हैं, तो करीब 33 प्रतिशत का अतिरिक्त चार्ज हावी होता है और रैपिंग ऑप्शन्स सामने आते हैं। जुड़ा हुआ एन्कोडिंग आर्टिकल राउंड ट्रिप के उस आधे हिस्से को पूरा-पूरा कवर करता है, इसलिए जब आप दूसरी दिशा में भेजने के लिए तैयार हों, तो वहीं चले जाइए।
अंतिम अपडेट: 2026-09-08
संबंधित लेख: Swift में Base64 एन्कोडिंग: एक सम्पूर्ण गाइड