JavaScript में base64 एन्कोडिंग स्ट्रिंग की बारीकियां

मैट जोसेफ़
मैट जोसेफ़

Base64 एन्कोडिंग और डिकोडिंग, बाइनरी कॉन्टेंट में बदलाव का एक आम तरीका है. इसे वेब पर सुरक्षित टेक्स्ट के तौर पर दिखाया जाता है. इसका इस्तेमाल आम तौर पर, इनलाइन इमेज जैसे डेटा यूआरएल के लिए किया जाता है.

JavaScript की स्ट्रिंग में base64 एन्कोडिंग और डिकोडिंग लागू करने पर क्या होता है? इस पोस्ट में, इससे बचने की बारीकियों और आम दिक्कतों के बारे में बताया गया है.

btoa() और atob()

JavaScript में, base64 को कोड में बदलने और डिकोड करने के मुख्य फ़ंक्शन हैं btoa() और atob(). btoa(), एक स्ट्रिंग से base64 कोड में बदली गई स्ट्रिंग में जाता है और atob() डिकोड वापस करता है.

नीचे इसका एक छोटा सा उदाहरण दिया गया है:

// A really plain string that is just code points below 128.
const asciiString = 'hello';

// This will work. It will print:
// Encoded string: [aGVsbG8=]
const asciiStringEncoded = btoa(asciiString);
console.log(`Encoded string: [${asciiStringEncoded}]`);

// This will work. It will print:
// Decoded string: [hello]
const asciiStringDecoded = atob(asciiStringEncoded);
console.log(`Decoded string: [${asciiStringDecoded}]`);

माफ़ करें, जैसा कि एमडीएन दस्तावेज़ में बताया गया है, यह सिर्फ़ उन स्ट्रिंग के साथ काम करता है जिनमें ASCII वर्ण या ऐसे वर्ण हों जिन्हें एक बाइट से दिखाया जा सकता है. दूसरे शब्दों में, यह यूनिकोड के साथ काम नहीं करेगा.

यह देखने के लिए कि क्या होता है, नीचे दिया गया कोड आज़माएं:

// Sample string that represents a combination of small, medium, and large code points.
// This sample string is valid UTF-16.
// 'hello' has code points that are each below 128.
// '⛳' is a single 16-bit code units.
// '❤️' is a two 16-bit code units, U+2764 and U+FE0F (a heart and a variant).
// '🧀' is a 32-bit code point (U+1F9C0), which can also be represented as the surrogate pair of two 16-bit code units '\ud83e\uddc0'.
const validUTF16String = 'hello⛳❤️🧀';

// This will not work. It will print:
// DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.
try {
  const validUTF16StringEncoded = btoa(validUTF16String);
  console.log(`Encoded string: [${validUTF16StringEncoded}]`);
} catch (error) {
  console.log(error);
}

स्ट्रिंग में मौजूद किसी भी इमोजी की वजह से गड़बड़ी हो सकती है. यूनिकोड की वजह से यह समस्या क्यों होती है?

इसे समझने के लिए, चलिए कंप्यूटर साइंस और JavaScript, दोनों के बारे में गहराई से जानते हैं और इसे समझने की कोशिश करते हैं.

यूनिकोड और JavaScript में स्ट्रिंग

यूनिकोड, कैरेक्टर एन्कोडिंग के लिए मौजूदा ग्लोबल स्टैंडर्ड है या किसी खास वर्ण को नंबर असाइन करने का तरीका है, ताकि उसे कंप्यूटर सिस्टम में इस्तेमाल किया जा सके. यूनिकोड के बारे में ज़्यादा जानने के लिए, W3C का यह लेख पढ़ें.

यूनिकोड में वर्णों और उनसे जुड़ी संख्याओं के कुछ उदाहरण:

  • घं - 104
  • ñ - 241
  • ❤ - 2764
  • ❤️ - छिपे हुए मॉडिफ़ायर नंबर 65039 के साथ 2764
  • ⛳ - 9971
  • करें - 129472

हर वर्ण को दिखाने वाली संख्याओं को "कोड पॉइंट" कहा जाता है. हर किरदार के लिए, "कोड पॉइंट" को एक पते के तौर पर समझा जा सकता है. लाल रंग के दिल वाले इमोजी में, असल में दो कोड पॉइंट होते हैं: एक दिल के लिए और दूसरा रंग "अलग" के लिए और दूसरा हमेशा लाल रंग का.

यूनिकोड में इन कोड पॉइंट को लेकर, उन्हें बाइट के ऐसे क्रम में बनाने के दो आम तरीके हैं जिन्हें कंप्यूटर लगातार समझ सके: UTF-8 और UTF-16.

इसका एक ज़्यादा आसान व्यू यह है:

  • UTF-8 में, कोड पॉइंट एक से चार बाइट (8 बिट प्रति बाइट) के बीच इस्तेमाल कर सकता है.
  • UTF-16 में, कोड पॉइंट हमेशा दो बाइट (16 बिट) का होता है.

खास तौर पर, JavaScript स्ट्रिंग को UTF-16 के रूप में प्रोसेस करता है. इससे, btoa() जैसे फ़ंक्शन काम नहीं करते. यह फ़ंक्शन सिर्फ़ यह मानकर काम करता है कि स्ट्रिंग का हर वर्ण एक बाइट से मैप होता है. एमडीएन पर साफ़ तौर पर इस बारे में बताया गया है:

btoa() वाला तरीका, किसी बाइनरी स्ट्रिंग यानी कि ऐसी स्ट्रिंग से Base64 कोड में बदली गई ASCII स्ट्रिंग बनाता है जिसमें स्ट्रिंग के हर वर्ण को बाइनरी डेटा का बाइट माना जाता है.

अब आपको पता है कि JavaScript में वर्णों को अक्सर एक से ज़्यादा बाइट की ज़रूरत होती है. अगले सेक्शन में बताया गया है कि base64 एन्कोडिंग और डिकोडिंग के लिए इस मामले को कैसे मैनेज किया जाए.

यूनिकोड के साथ btoa() और atob()

जैसा कि अब आपको पता है कि गड़बड़ी होने की वजह, हमारी स्ट्रिंग में ऐसे वर्ण मौजूद हैं जो UTF-16 में एक बाइट से बाहर होते हैं.

अच्छी बात यह है कि base64 पर MDN लेख में इस "यूनिकोड की समस्या" को हल करने के लिए कुछ मददगार सैंपल कोड दिए गए हैं. पिछले उदाहरण के साथ काम करने के लिए, इस कोड में बदलाव किया जा सकता है:

// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function bytesToBase64(bytes) {
  const binString = String.fromCodePoint(...bytes);
  return btoa(binString);
}

// Sample string that represents a combination of small, medium, and large code points.
// This sample string is valid UTF-16.
// 'hello' has code points that are each below 128.
// '⛳' is a single 16-bit code units.
// '❤️' is a two 16-bit code units, U+2764 and U+FE0F (a heart and a variant).
// '🧀' is a 32-bit code point (U+1F9C0), which can also be represented as the surrogate pair of two 16-bit code units '\ud83e\uddc0'.
const validUTF16String = 'hello⛳❤️🧀';

// This will work. It will print:
// Encoded string: [aGVsbG/im7PinaTvuI/wn6eA]
const validUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(validUTF16String));
console.log(`Encoded string: [${validUTF16StringEncoded}]`);

// This will work. It will print:
// Decoded string: [hello⛳❤️🧀]
const validUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(validUTF16StringEncoded));
console.log(`Decoded string: [${validUTF16StringDecoded}]`);

यहां दिया गया तरीका बताता है कि यह कोड, स्ट्रिंग को कोड में बदलने के लिए क्या करता है:

  1. UTF-16 कोड में बदली गई JavaScript स्ट्रिंग को लेने के लिए, TextEncoder इंटरफ़ेस का इस्तेमाल करें. साथ ही, TextEncoder.encode() का इस्तेमाल करके, इसे UTF-8-एन्कोडेड बाइट की स्ट्रीम में बदलें.
  2. इससे Uint8Array दिखता है, जो JavaScript में कम इस्तेमाल किया जाने वाला डेटा टाइप है. साथ ही, यह TypedArray का सब-क्लास है.
  3. उस Uint8Array को लें और इसे bytesToBase64() फ़ंक्शन के लिए दें. यह Uint8Array में मौजूद हर बाइट को कोड पॉइंट के तौर पर दिखाने के लिए String.fromCodePoint() का इस्तेमाल करता है. इससे एक स्ट्रिंग बनाई जाती है. इससे उन कोड पॉइंट की एक स्ट्रिंग बनती है जिन्हें एक बाइट के तौर पर दिखाया जा सकता है.
  4. वह स्ट्रिंग लें और उसे base64 कोड में बदलने के लिए btoa() का इस्तेमाल करें.

डिकोड करने की प्रोसेस वही होती है, लेकिन पूरी तरह से उलटी.

यह काम करता है, क्योंकि Uint8Array और स्ट्रिंग के बीच का चरण इस बात की गारंटी देता है कि JavaScript में स्ट्रिंग को UTF-16, यानी दो-बाइट एन्कोडिंग के रूप में दिखाया जाता है. साथ ही, हर दो बाइट वाला कोड पॉइंट हमेशा 128 से कम होता है.

यह कोड ज़्यादातर मामलों में ठीक से काम करता है, लेकिन दूसरे मामलों में यह काम नहीं करेगा.

साइलेंट फ़ेलियर केस

उसी कोड का इस्तेमाल करें, लेकिन अलग स्ट्रिंग के साथ:

// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function bytesToBase64(bytes) {
  const binString = String.fromCodePoint(...bytes);
  return btoa(binString);
}

// Sample string that represents a combination of small, medium, and large code points.
// This sample string is invalid UTF-16.
// 'hello' has code points that are each below 128.
// '⛳' is a single 16-bit code units.
// '❤️' is a two 16-bit code units, U+2764 and U+FE0F (a heart and a variant).
// '🧀' is a 32-bit code point (U+1F9C0), which can also be represented as the surrogate pair of two 16-bit code units '\ud83e\uddc0'.
// '\uDE75' is code unit that is one half of a surrogate pair.
const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';

// This will work. It will print:
// Encoded string: [aGVsbG/im7PinaTvuI/wn6eA77+9]
const partiallyInvalidUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(partiallyInvalidUTF16String));
console.log(`Encoded string: [${partiallyInvalidUTF16StringEncoded}]`);

// This will work. It will print:
// Decoded string: [hello⛳❤️🧀�]
const partiallyInvalidUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(partiallyInvalidUTF16StringEncoded));
console.log(`Decoded string: [${partiallyInvalidUTF16StringDecoded}]`);

अगर डीकोड करने के बाद आखिरी वर्ण ( ) को लिया जाता है और उसकी हेक्स वैल्यू की जांच की जाती है, तो आपको दिखेगा कि यह मूल \uDE75 की जगह \uFFFD है. यह फ़ेल नहीं होता या कोई गड़बड़ी नहीं होती है, लेकिन इनपुट और आउटपुट डेटा बिना किसी रुकावट के बदल जाते हैं. ऐसा क्यों है?

JavaScript API के हिसाब से स्ट्रिंग अलग-अलग होती हैं

जैसा कि पहले बताया गया है, JavaScript स्ट्रिंग को UTF-16 के रूप में प्रोसेस करता है. हालांकि, UTF-16 स्ट्रिंग की एक यूनीक प्रॉपर्टी होती है.

उदाहरण के तौर पर चीज़ इमोजी को लें. इमोजी (≡) में 129472 का यूनिकोड कोड पॉइंट है. माफ़ करें, 16-बिट नंबर के लिए ज़्यादा से ज़्यादा वैल्यू 65535 है! आइए जानते हैं कि UTF-16, इस संख्या को कैसे दिखाता है?

UTF-16 में एक सिद्धांत है सरोगेट पेयर. इसे इस तरह से समझा जा सकता है:

  • जोड़े का पहला नंबर बताता है कि किस "किताब" में खोज करनी है. इसे "सरोगेट" कहा जाता है.
  • जोड़े में दूसरा नंबर "बुक" में एंट्री है.

जैसा कि आपने सोचा होगा, कभी-कभी सिर्फ़ संख्या को किताब की जानकारी देने की वजह से समस्या हो सकती है. हालांकि, किताब में असली संख्या न होना. यूटीएफ़-16 में, इसे लोन सरोगेट कहा जाता है.

JavaScript में यह खास तौर पर चुनौती भरा काम होता है, क्योंकि कुछ एपीआई एकांत सरोगेट होने के बावजूद काम करते हैं, जबकि दूसरे काम नहीं करते.

ऐसे में, base64 से वापस डिकोड करते समय, TextDecoder का इस्तेमाल किया जा रहा है. खास तौर पर, TextDecoder की डिफ़ॉल्ट वैल्यू यह जानकारी देती है:

यह डिफ़ॉल्ट रूप से false पर सेट होता है. इसका मतलब है कि डिकोडर, खराब डेटा को बदले गए वर्ण से बदल देता है.

आपने पहले जो वर्ण देखा था, उसे हेक्साडेसिमल में \uFFFD के तौर पर दिखाया गया है. यह बदले जाने वाले वर्ण है. UTF-16 में, अकेले सरोगेट वाली स्ट्रिंग को "खराब" या "सही तरह से बनाई नहीं गई" माना जाता है.

वेब के ऐसे कई स्टैंडर्ड हैं (उदाहरण 1, 2, 3, 4) जो सटीक तौर पर बताते हैं कि किसी गलत स्ट्रिंग से एपीआई के व्यवहार पर कब असर पड़ता है. खास तौर पर, TextDecoder उन एपीआई में से एक है. टेक्स्ट को प्रोसेस करने से पहले, यह पक्का कर लेना अच्छा होता है कि स्ट्रिंग अच्छी तरह से बनाई गई हों.

अच्छी तरह से बनाई गई स्ट्रिंग की जांच करना

हाल ही के ब्राउज़र में अब इस काम के लिए फ़ंक्शन मौजूद है: isWellFormed().

ब्राउज़र सहायता

  • 111
  • 111
  • 119
  • 78 जीबी में से

सोर्स

ऐसा ही नतीजा पाने के लिए, encodeURIComponent() का इस्तेमाल करें. अगर स्ट्रिंग में कोई सरोगेट है, तो URIError गड़बड़ी हो सकती है.

अगर उपलब्ध है, तो नीचे दिया गया फ़ंक्शन, isWellFormed() का इस्तेमाल करता है और उपलब्ध न होने पर encodeURIComponent() का इस्तेमाल करता है. इसी तरह के कोड का इस्तेमाल, isWellFormed() के लिए पॉलीफ़िल बनाने में किया जा सकता है.

// Quick polyfill since older browsers do not support isWellFormed().
// encodeURIComponent() throws an error for lone surrogates, which is essentially the same.
function isWellFormed(str) {
  if (typeof(str.isWellFormed)!="undefined") {
    // Use the newer isWellFormed() feature.
    return str.isWellFormed();
  } else {
    // Use the older encodeURIComponent().
    try {
      encodeURIComponent(str);
      return true;
    } catch (error) {
      return false;
    }
  }
}

पूरी जानकारी का इस्तेमाल करना

अब जब आपने यूनिकोड और लोन सरोगेट, दोनों को ही हैंडल करने का तरीका जान लिया है, तो सब कुछ एक साथ जोड़कर ऐसा कोड बनाया जा सकता है जो सभी केस को हैंडल कर सके और ऐसा टेक्स्ट बदले बिना किया जा सके.

// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function bytesToBase64(bytes) {
  const binString = String.fromCodePoint(...bytes);
  return btoa(binString);
}

// Quick polyfill since Firefox and Opera do not yet support isWellFormed().
// encodeURIComponent() throws an error for lone surrogates, which is essentially the same.
function isWellFormed(str) {
  if (typeof(str.isWellFormed)!="undefined") {
    // Use the newer isWellFormed() feature.
    return str.isWellFormed();
  } else {
    // Use the older encodeURIComponent().
    try {
      encodeURIComponent(str);
      return true;
    } catch (error) {
      return false;
    }
  }
}

const validUTF16String = 'hello⛳❤️🧀';
const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';

if (isWellFormed(validUTF16String)) {
  // This will work. It will print:
  // Encoded string: [aGVsbG/im7PinaTvuI/wn6eA]
  const validUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(validUTF16String));
  console.log(`Encoded string: [${validUTF16StringEncoded}]`);

  // This will work. It will print:
  // Decoded string: [hello⛳❤️🧀]
  const validUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(validUTF16StringEncoded));
  console.log(`Decoded string: [${validUTF16StringDecoded}]`);
} else {
  // Not reached in this example.
}

if (isWellFormed(partiallyInvalidUTF16String)) {
  // Not reached in this example.
} else {
  // This is not a well-formed string, so we handle that case.
  console.log(`Cannot process a string with lone surrogates: [${partiallyInvalidUTF16String}]`);
}

इस कोड में कई तरह के ऑप्टिमाइज़ेशन किए जा सकते हैं. जैसे, पॉलीफ़िल को सामान्य बनाना, अकेले सरोगेट की जगह बिना थ्रो के TextDecoder पैरामीटर को बदलना, और ऐसे ही कई दूसरे काम.

इस जानकारी और कोड की मदद से, आपको खराब स्ट्रिंग को ठीक करने के बारे में भी साफ़ तौर पर फ़ैसले लेने में मदद मिलती है. उदाहरण के लिए, डेटा को अस्वीकार करना, डेटा बदलने की सुविधा को साफ़ तौर पर चालू करना या बाद में विश्लेषण करते समय कोई गड़बड़ी दिखाना.

base64 एन्कोडिंग और डिकोडिंग के लिए एक मूल्यवान उदाहरण होने के अलावा, यह पोस्ट इसका उदाहरण भी बताती है कि सावधानी से टेक्स्ट प्रोसेसिंग विशेष रूप से क्यों ज़रूरी है, खास तौर पर तब, जब टेक्स्ट डेटा उपयोगकर्ता के जनरेट किए गए या बाहरी सोर्स से आ रहा हो.