জাভাস্ক্রিপ্টে বেস64 এনকোডিং স্ট্রিংগুলির সূক্ষ্মতা

বেস 64 এনকোডিং এবং ডিকোডিং হল ওয়েব-সেফ টেক্সট হিসাবে উপস্থাপন করা বাইনারি বিষয়বস্তুকে রূপান্তরিত করার একটি সাধারণ রূপ। এটি সাধারণত ডেটা URL এর জন্য ব্যবহৃত হয়, যেমন ইনলাইন চিত্র।

আপনি যখন জাভাস্ক্রিপ্টের স্ট্রিংগুলিতে base64 এনকোডিং এবং ডিকোডিং প্রয়োগ করেন তখন কী হয়? এই পোস্টটি এড়ানোর জন্য সূক্ষ্মতা এবং সাধারণ ত্রুটিগুলি অন্বেষণ করে৷

btoa() এবং atob()

জাভাস্ক্রিপ্টে 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}]`);

দুর্ভাগ্যবশত, MDN ডক্স দ্বারা উল্লিখিত হিসাবে, এটি শুধুমাত্র 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);
}

স্ট্রিং এর যে কোনো একটি ইমোজি ত্রুটির কারণ হবে৷ ইউনিকোড কেন এই সমস্যা সৃষ্টি করে??

বুঝতে, কম্পিউটার বিজ্ঞান এবং জাভাস্ক্রিপ্ট উভয় ক্ষেত্রেই, আসুন একধাপ পিছিয়ে যাই এবং স্ট্রিংগুলি বুঝতে পারি।

ইউনিকোড এবং জাভাস্ক্রিপ্টে স্ট্রিং

ইউনিকোড হ'ল অক্ষর এনকোডিংয়ের জন্য বর্তমান বিশ্বব্যাপী মান, বা একটি নির্দিষ্ট অক্ষরকে একটি সংখ্যা নির্ধারণ করার অনুশীলন যাতে এটি কম্পিউটার সিস্টেমে ব্যবহার করা যায়। ইউনিকোডের গভীরে ডুব দেওয়ার জন্য, এই W3C নিবন্ধটি দেখুন।

ইউনিকোডে অক্ষর এবং তাদের সংশ্লিষ্ট সংখ্যার কিছু উদাহরণ:

  • h - 104
  • ñ - 241
  • ❤ - 2764
  • ❤️ - 65039 নম্বরের একটি লুকানো সংশোধক সহ 2764
  • ⛳ - 9971
  • 🧀 - 129472

প্রতিটি অক্ষরের প্রতিনিধিত্বকারী সংখ্যাগুলিকে "কোড পয়েন্ট" বলা হয়। আপনি প্রতিটি অক্ষরের ঠিকানা হিসাবে "কোড পয়েন্ট" ভাবতে পারেন। রেড হার্ট ইমোজিতে, আসলে দুটি কোড পয়েন্ট রয়েছে: একটি হার্টের জন্য এবং একটি রঙের "পরিবর্তন" এবং এটিকে সর্বদা লাল করতে।

ইউনিকোডের এই কোড পয়েন্টগুলি নেওয়ার এবং কম্পিউটারগুলি ধারাবাহিকভাবে ব্যাখ্যা করতে পারে এমন বাইটের ক্রমগুলিতে তৈরি করার দুটি সাধারণ উপায় রয়েছে: UTF-8 এবং UTF-16৷

একটি অতি সরলীকৃত দৃষ্টিভঙ্গি হল:

  • UTF-8-এ, একটি কোড পয়েন্ট এক থেকে চার বাইট (প্রতি বাইটে 8 বিট) ব্যবহার করতে পারে।
  • UTF-16-এ, একটি কোড পয়েন্ট সবসময় দুই বাইট (16 বিট) হয়।

গুরুত্বপূর্ণভাবে, জাভাস্ক্রিপ্ট স্ট্রিংগুলিকে UTF-16 হিসাবে প্রসেস করে। এটি btoa() মতো ফাংশনগুলিকে ভেঙে দেয়, যা কার্যকরভাবে এই ধারণার উপর কাজ করে যে স্ট্রিংয়ের প্রতিটি অক্ষর একটি একক বাইটে মানচিত্র করে। এটি MDN- এ স্পষ্টভাবে বলা হয়েছে:

btoa() পদ্ধতিটি একটি বাইনারি স্ট্রিং থেকে একটি Base64-এনকোডেড ASCII স্ট্রিং তৈরি করে (অর্থাৎ, একটি স্ট্রিং যেখানে স্ট্রিংয়ের প্রতিটি অক্ষরকে বাইনারি ডেটার বাইট হিসাবে বিবেচনা করা হয়)।

এখন আপনি জানেন যে জাভাস্ক্রিপ্টের অক্ষরগুলির জন্য প্রায়শই একাধিক বাইটের প্রয়োজন হয়, পরবর্তী বিভাগটি দেখায় যে বেস64 এনকোডিং এবং ডিকোডিংয়ের জন্য এই কেসটি কীভাবে পরিচালনা করা যায়।

ইউনিকোড সহ btoa() এবং atob()

আপনি এখন জানেন, UTF-16-এ একটি একক বাইটের বাইরে বসে থাকা অক্ষর ধারণকারী আমাদের স্ট্রিং-এর কারণে যে ত্রুটিটি নিক্ষেপ করা হচ্ছে।

সৌভাগ্যবশত, বেস64-এর 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 এনকোড করা জাভাস্ক্রিপ্ট স্ট্রিং নিতে TextEncoder ইন্টারফেস ব্যবহার করুন এবং TextEncoder.encode() ব্যবহার করে এটিকে UTF-8-এনকোডেড বাইটের একটি স্ট্রীমে রূপান্তর করুন।
  2. এটি একটি Uint8Array প্রদান করে, যা জাভাস্ক্রিপ্টে একটি কম ব্যবহৃত ডেটা টাইপ এবং এটি TypedArray এর একটি সাবক্লাস।
  3. সেই Uint8Array নিন এবং এটিকে bytesToBase64() ফাংশনে প্রদান করুন, যা String.fromCodePoint() ব্যবহার করে Uint8Array এর প্রতিটি বাইটকে একটি কোড পয়েন্ট হিসাবে বিবেচনা করে এবং এটি থেকে একটি স্ট্রিং তৈরি করে, যার ফলে কোড পয়েন্টগুলির একটি স্ট্রিং তৈরি হয় যা সবই করতে পারে। একটি একক বাইট হিসাবে প্রতিনিধিত্ব করা.
  4. সেই স্ট্রিংটি নিন এবং এটিকে বেস64 এনকোড করতে 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 । এটি ব্যর্থ হয় না বা একটি ত্রুটি নিক্ষেপ করে না, তবে ইনপুট এবং আউটপুট ডেটা নীরবে পরিবর্তিত হয়েছে। কেন?

জাভাস্ক্রিপ্ট API দ্বারা স্ট্রিং পরিবর্তিত হয়

পূর্বে বর্ণিত হিসাবে, জাভাস্ক্রিপ্ট স্ট্রিংগুলিকে UTF-16 হিসাবে প্রক্রিয়া করে। কিন্তু UTF-16 স্ট্রিংগুলির একটি অনন্য বৈশিষ্ট্য রয়েছে।

একটি উদাহরণ হিসাবে পনির ইমোজি নিন। ইমোজি (🧀) এর একটি ইউনিকোড কোড পয়েন্ট আছে 129472 । দুর্ভাগ্যবশত, একটি 16-বিট নম্বরের সর্বোচ্চ মান হল 65535! তাহলে কিভাবে UTF-16 এই অনেক বেশি সংখ্যার প্রতিনিধিত্ব করে?

UTF-16 এর একটি ধারণা রয়েছে যাকে সারোগেট জোড়া বলা হয়। আপনি এটি এই ভাবে চিন্তা করতে পারেন:

  • জুটির প্রথম সংখ্যাটি নির্দিষ্ট করে যে কোন "বই" অনুসন্ধান করতে হবে। একে " সারোগেট " বলা হয়।
  • জোড়ায় দ্বিতীয় সংখ্যাটি "বই"-এ প্রবেশ।

আপনি যেমন কল্পনা করতে পারেন, কখনও কখনও কেবল বইটির প্রতিনিধিত্বকারী সংখ্যা থাকা সমস্যাযুক্ত হতে পারে, কিন্তু সেই বইটিতে প্রকৃত এন্ট্রি নয়। UTF-16-এ, এটি একাকী সারোগেট হিসাবে পরিচিত।

এটি জাভাস্ক্রিপ্টে বিশেষভাবে চ্যালেঞ্জিং, কারণ কিছু API একাকী সারোগেট থাকা সত্ত্বেও কাজ করে যখন অন্যরা ব্যর্থ হয়।

এই ক্ষেত্রে, base64 থেকে ফিরে ডিকোড করার সময় আপনি TextDecoder ব্যবহার করছেন। বিশেষ করে, TextDecoder জন্য ডিফল্টগুলি নিম্নলিখিতগুলি নির্দিষ্ট করে:

এটি ডিফল্ট করে মিথ্যা , যার অর্থ হল ডিকোডার একটি প্রতিস্থাপন অক্ষর দিয়ে বিকৃত ডেটা প্রতিস্থাপন করে।

যে � অক্ষরটি আপনি আগে দেখেছেন, যেটিকে হেক্সে \uFFFD হিসাবে উপস্থাপন করা হয়েছে, সেটি হল প্রতিস্থাপনের অক্ষর। UTF-16-এ, একাকী সারোগেট সহ স্ট্রিংগুলিকে "বিকৃত" বা "ভালভাবে গঠিত নয়" বলে মনে করা হয়।

বিভিন্ন ওয়েব স্ট্যান্ডার্ড রয়েছে (উদাহরণ 1 , 2 , 3 , 4 ) যেগুলি সঠিকভাবে নির্দিষ্ট করে যখন একটি বিকৃত স্ট্রিং API আচরণকে প্রভাবিত করে, কিন্তু উল্লেখযোগ্যভাবে TextDecoder হল সেই APIগুলির মধ্যে একটি৷ টেক্সট প্রসেসিং করার আগে স্ট্রিংগুলি ভালভাবে তৈরি হয়েছে তা নিশ্চিত করা ভাল অনুশীলন।

সুগঠিত স্ট্রিং জন্য পরীক্ষা করুন

খুব সাম্প্রতিক ব্রাউজারগুলির এখন এই উদ্দেশ্যে একটি ফাংশন রয়েছে: isWellFormed()

ব্রাউজার সমর্থন

  • ক্রোম: 111।
  • প্রান্ত: 111।
  • ফায়ারফক্স: 119।
  • সাফারি: 16.4.

উৎস

আপনি 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 প্যারামিটার পরিবর্তন করা এবং আরও অনেক কিছু।

এই জ্ঞান এবং কোডের সাহায্যে, আপনি কীভাবে বিকৃত স্ট্রিংগুলি পরিচালনা করবেন সে সম্পর্কে স্পষ্ট সিদ্ধান্ত নিতে পারেন, যেমন ডেটা প্রত্যাখ্যান করা বা স্পষ্টভাবে ডেটা প্রতিস্থাপন সক্ষম করা, বা পরবর্তী বিশ্লেষণের জন্য সম্ভবত একটি ত্রুটি নিক্ষেপ করা।

বেস64 এনকোডিং এবং ডিকোডিংয়ের জন্য একটি মূল্যবান উদাহরণ হওয়ার পাশাপাশি, এই পোস্টটি একটি উদাহরণ প্রদান করে যে কেন সতর্কতামূলক পাঠ্য প্রক্রিয়াকরণ বিশেষভাবে গুরুত্বপূর্ণ, বিশেষ করে যখন পাঠ্য ডেটা ব্যবহারকারী-উত্পাদিত বা বাহ্যিক উত্স থেকে আসছে।