C++ での Base 64 エンコーディングの実装
このチュートリアルでは、C++ の base_64
でのエンコーディングについて説明します。
最初に、base_64
エンコーディングと、それが必要な理由と場所について説明します。 後で、C++ での base_64
のエンコード/デコードについて説明します。
エンコード方式 Base_64
Base_64
はエンコーディング スキームへの追加です。 これは、バイナリ データを ASCII 文字列で表すという点で、バイナリからテキストへのエンコーディングに似ています。
違いは、base_64
エンコーディングが基数 64 への変換を使用することです。 Base_64
エンコーディング名は、塩基の数学的定義に由来します。
基数は、数体系の基数を表します。 同様に、base_2
には 0 と 1 の 2つの基本数字しかありません。
8 進数システムであるBase_8
には、0 から 7 までの 8つの基本数字があります。
同様に、base_16
には 0 から 15 までの 16 個の基本数字があり、A
から F
を使用して 10
から 15
を表します。 base_64
には、以下で構成される 64 個の基本数字があります。
- 26 大文字のアルファベット
- 26個の小さなアルファベット
- 0~9の10桁
- 2つの記号
+
と/
Base_64
エンコーディングは、ASCII を処理するように設計されたメディアを介してデータを転送するために一般的に使用されます。 base_64
は、メディアを介して送信されるデータの整合性を維持しようとします。
主なアプリケーションは、MIME 経由の電子メールであり、複雑なデータを XML で保存します。 Base_64
はプライバシー強化電子メール (PEM) とも呼ばれます。
Base_64
ステップのエンコード
エンコードの場合、データはバイナリ文字列としてあり、文字列内の各文字を操作する必要があります。 base_64
でエンコードするには、次の手順を実行する必要があります。
-
各文字の ASCII 値を取得します。
-
ASCII 値の 8 ビット バイナリを見つけます。
-
ステップ2で得た8ビットを、桁を並べ替えて6ビットに変換します(一部のビット操作を含む何らかの操作が必要です(後述))。
-
6 ビット バイナリを対応する 10 進数値に変換します。
-
base_64
(すでに説明したbase_64
の基本数字) を使用して、それぞれのbase_64
文字を各 10 進数値に割り当てます。
ここでは、8 ビット グループから 6 ビット グループへの変換であるステップ 3 の詳細について説明します。
8 ビット グループを 6 ビット グループに変換する手順
最初に説明したように、Base_64
エンコーディングでは、64
個の主要な文字/数字がありますが、通常はデータをバイト単位で読み書きします。 1 バイトは 8 ビットで、0
から 255
を格納できます。つまり、1 バイトで一意の 256
を表すことができます。
6 ビットは 64
の一意の値を表すことができます。最後の 2 ビットは 0 のままにして、各バイトが Base_64
エンコード方式の 1 桁/文字のみを格納するようにする必要があります。
各文字/ASCII 値は 8 ビットを使用します。 したがって、各バイトの 2 ビットを調整するには、元のデータよりも多くのストレージが必要になります。
Base_64
エンコーディングでは、データを失うことなく 6 ビットに変換する必要があります。
8 と 6 の LCM を取ると、24 になります。3 バイトには 24 ビットがありますが、8 ビットのうち 6 ビットを使用すると (最後の 2 ビットは使用されません)、24 ビットには 4 バイトが必要です。 したがって、データを失うことなく、3つの 8 ビット グループのそれぞれを 4つの 6 ビット グループに変換できます。
最初のステップは、データを 3 バイトのセットにグループ化することです。 最後のグループのバイト数が少ない場合、値が 0 のバイトを追加してグループを完成させます。
次に、3 バイトの各セットは、次の操作を使用して 4 バイトにグループ化されます。 3 バイトのセットを t1, t2 & t3
として、4 バイトを f1, f2, f3 & f4
として考えてみましょう。
f1 = ( t1 & 0xfc ) >> 2
マスク 0xfc
(バイナリ 11111100
に相当) を考慮し、ビット単位で適用し、セットの最初のバイトとマスクの間で操作します。 次に、ビット単位の演算結果に対して右シフトを 2 回使用します。
シフト操作は左の 6 ビットを右に転送し、最後の 2 ビットは 0 になります。
マスク 0xfc
の最初の 2 ビットは 0 です。 演算によってセットの最初のバイトの最初の 2 ビットが 0 になる場合 (つまり、最初のバイトの最後の 6 ビットが考慮されることを意味します)、最初の 2 ビット (この演算では無視されます) は次のプロセスで考慮されます。
f2 = ( ( t1 & 0x03 ) << 4 ) + ( ( t2 & 0xf0 ) >> 4 )
ここでは、演算の最初のバイトにマスク 0x03 00000011
が適用されます (つまり、最初の 2 ビットのみが考慮され、最後の 6 ビットは前の演算で既に考慮されています)。 シフト操作は、最初のバイトの結果の 2 ビットを左に転送し、式の 5 番目と 6 番目のビットにします。
マスク 0xf0 11110000
は、演算の 2 番目のバイトに適用されます (つまり、最後の 4 ビットのみが考慮されます)。 シフト操作は、結果の 4 ビットを右に転送して、式の最初の 4 ビットにします。
式の最初の部分では 5 番目と 6 番目のビットがオンになり、2 番目の部分では最初の 4 ビットがオンになり、全体として、最初の 6 ビットがオンになり、最後のビットがオフになります。
最後に、それらを組み合わせて、最後の 2 ビットがオフになっているバイトを取得します。 このステップでは、6 ビットの別のバイトを取得しました。最初のバイトが完了し、2 番目のバイトの最初の 4 ビットが考慮されます。
f3 = ( ( t2 & 0x0f ) << 2 ) + ( ( t3 & 0xc0 ) >> 6 )
マスク 0x0f 00001111
は、操作のために 2 番目のバイトに適用されます (つまり、最初の 4 ビットのみが考慮されます)。 シフト操作は、結果の 4 ビットを左に転送して式の 3、4、5、6 ビットにし、最初の 2 ビット用のスペースを作成します。
次に、操作のためにマスク 0xc0 11000000
が 3 番目のバイトに適用されます (つまり、最初の 2 ビットのみが考慮されます)。 シフト操作は、結果の 2 ビットを右に転送して、式の最初と 2 番目のビットにします。
最後に、両方の結果を組み合わせて、6 ビット グループの 3 番目のバイトを取得します。 繰り返しますが、セットでは、セットの 2 番目のバイトと 3 番目のバイトの 2 ビットが完了しました。
f4 = t3 & 0x3f
最後に、3 番目のバイトには操作のみがあり、マスク 0x3f 00111111
は最初の 6 ビットがオンで、最後の 2 ビットがオフです。 3 番目のバイトの操作では、3 番目のバイトの残りの 6 ビットが考慮されます。
base_64
で使用される 64 個の基本数字については既に説明しました。 次のステップでは、4 バイトのセット (ビット演算を使用して取得) の各バイトが base_64
に変換され、文字列に連結されます。
PLAY
という単語をエンコードしてみましょう。 最初のステップでは、それぞれ 3 キャラクターのセットを作成します。 最初のセットには、PLA
があります。
次の段階では、Y\0\0
があります。 ここで、\0
は、セットを完成させるために追加されたヌル文字です。
これらの各文字の ASCII は 80 76 65 89 0 0
です。 対応するバイナリ値は 01010000 01001000 01000001 01011001
です。
では、ビット操作をしましょう。
f1 = 01010000 & 11111100 = 01010000 >> 2 = 00010100 = 20
01010000 & 00000011 = 0000000 << 4 = 00000000
式の最初の部分01001000 & 11110000 = 01010000 >> 4 = 00000101
式の 2 番目の部分f2 = 00000000 + 00000101 = 00000101 = 5
、最初の部分と 2 番目の部分の結果を加算01001000 & 00001111 = 00001000 << 2 = 00100000
式の最初の部分01000001 & 11000000 = 01000000 >> 4 = 00000100
式の 2 番目の部分f3 = 00100000 + 00000100 = 00100100 = 36
、最初の部分と 2 番目の部分の結果を加算f4 = 01000001 & 00000011 = 00000001 = 1
次に、2 番目と 3 番目の値が 0 である次のセットで操作を繰り返します。 したがって、結果は次のようになります。
f1 = 00010110 = 21
f2 = 00010000 = 16
f3 = 0
f4 = 0
次に、これらの値を base_64
に変換する必要があります。 また、最後の 2 バイトにいくつかのセンチネル/特殊文字を配置して、デコード プロセスがそれらを認識し、それに応じてデコードできるようにする必要があります。
最初のセットでは、f1= 20, f2 = 5, f3 = 36 & f4 = 1
. 対応する base_64
値は UFkB
になります。
次のセットは f1 = 21, f2 = 16, f3 = 0 & f4 = 0
. ここでも、対応する base_64
値は VQ^^
になり、キャレット記号が特殊文字として使用されるため、文字列全体は UFkBV^^
になります。
デコード プロセスは単純に逆のプロセスです。 以下の C++ コードから両方のメソッドをすばやく取得できます。
C++ でのBase_64
エンコーディングの実装
C++ でエンコードを行うのは簡単なプロセスです。 C++ で (説明した手順を) すぐに実装できます。
段階的に説明し、最後に 2つの例を含む完全なコードを示します。
まず、base_64
変換のために、base_64
の基本的な数字/文字を含む定数文字列を定義します。
const string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
関数のコーディングとデコードについて説明する前に、最初にいくつかの定義があります。 主に、エンコードとデコードに使用されるマスクがいくつかあります。
これらのマスクのうち 6つについては、8 ビット グループから 6 ビット グループへの変換を説明する際に既に説明しました。
これらのマスクの一部は、デコード プロセスで 6 ビット グループから 8 ビット グループに変換するために使用されます。さらに、さらに 2つのマスクが必要になります。 全部で8枚のマスクがあります。
typedef unsigned char UC;
typedef unsigned int UI;
#define EXTRA '^'
#define MASK1 0xfc
#define MASK2 0x03
#define MASK3 0xf0
#define MASK4 0x0f
#define MASK5 0xc0
#define MASK6 0x3f
#define MASK7 0x30
#define MASK8 0x3c
次に、エンコード機能について考えます。 3キャラセットでお作りします。
次に、それらをビット演算を使用して 4 文字のグループに変換します。詳細については、既に説明しました。 最後に、4 文字のグループの各バイトを変換して連結し、エンコードされた文字列を作成します。
コードは次のとおりです。
string encode_base64(UC const* buf, UI bufLen) {
string encoded = "";
UI i = 0, j = 0, k = 0;
UC temp_a_3[3], temp_4[4];
for (i = 0; i < bufLen; i += 3) {
for (j = i, k = 0; j < bufLen && j < i + 3; j++) temp_a_3[k++] = *(buf++);
for (; k < 3; k++) temp_a_3[k] = '\0';
temp_4[0] = (temp_a_3[0] & MASK1) >> 2;
temp_4[1] = ((temp_a_3[0] & MASK2) << 4) + ((temp_a_3[1] & MASK3) >> 4);
temp_4[2] = ((temp_a_3[1] & MASK4) << 2) + ((temp_a_3[2] & MASK5) >> 6);
temp_4[3] = temp_a_3[2] & MASK6;
for (j = i, k = 0; j < bufLen + 1 && j < i + 4; j++, k++)
encoded += base64_chars[temp_4[k]];
for (; k < 4; k++) encoded += EXTRA; // sentinal value
}
return encoded;
}
この関数は 2つのパラメーターを受け取ります。1つ目は生データ (コーディングのために送信される) で、2つ目はメッセージの長さです。 サイズ 3 と 4 の 2つの配列を宣言しました。ループ内では、サイズ 3 の最初の配列にデータを格納します。
次に、最後のセットのバイト数が少ない場合は、null 文字を追加して最後のセットを完成させます。 次に、8 ビット データを 6 ビット単位の操作に変換する 4つのステートメントがあります。
最後に、最後から 2 番目のループで、4つの 6 ビット文字のグループを base_64
に変換します。
最後のループでは、余分な文字を格納して 4 バイトのセットを完成させます。 次に、decode 関数があります。
vector<UC> decode_base64(string const& encoded) {
UI i = 0, j = 0, k = 0, in_len = encoded.size();
UC temp_a_3[3], temp_4[4];
vector<UC> decoded;
for (i = 0; i < in_len; i += 4) {
for (j = i, k = 0; j < i + 4 && encoded[j] != EXTRA; j++)
temp_4[k++] = base64_chars.find(encoded[j]);
for (; k < 4; k++) temp_4[k++] = '\0';
temp_a_3[0] = (temp_4[0] << 2) + ((temp_4[1] & MASK7) >> 4);
temp_a_3[1] = ((temp_4[1] & MASK4) << 4) + ((temp_4[2] & MASK8) >> 2);
temp_a_3[2] = ((temp_4[2] & MASK2) << 6) + temp_4[3];
for (j = i, k = 0; k < 3 && encoded[j + 1] != EXTRA; j++, k++)
decoded.push_back(temp_a_3[k]);
}
return decoded;
}
この関数は、エンコードされたメッセージを受け取り、次の手順を含む逆の操作を行います。
base_64
文字セットから取得した各文字のインデックスを取得し、4 バイトのセットを作成します。- ここでも、エンコード プロセスで保存した特殊文字に対して 0 を追加します。
- 次に、逆ビット操作で 4 バイトのセットを 3 バイトのセットに変換します (ここでは、これらの操作の詳細には立ち入りません)。
- 最後に、3 バイトのセットを結合して、結合されたデコードされたメッセージを取得します。
最後に、コーディングとエンコーディングの 2つの例を含む完全なコードを示します。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
typedef unsigned char UC;
typedef unsigned int UI;
#define EXTRA '^'
#define MASK1 0xfc
#define MASK2 0x03
#define MASK3 0xf0
#define MASK4 0x0f
#define MASK5 0xc0
#define MASK6 0x3f
#define MASK7 0x30
#define MASK8 0x3c
const string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
string encode_base64(UC const* buf, UI bufLen) {
string encoded = "";
UI i = 0, j = 0, k = 0;
UC temp_a_3[3], temp_4[4];
for (i = 0; i < bufLen; i += 3) {
for (j = i, k = 0; j < bufLen && j < i + 3; j++) temp_a_3[k++] = *(buf++);
for (; k < 3; k++) temp_a_3[k] = '\0';
temp_4[0] = (temp_a_3[0] & MASK1) >> 2;
temp_4[1] = ((temp_a_3[0] & MASK2) << 4) + ((temp_a_3[1] & MASK3) >> 4);
temp_4[2] = ((temp_a_3[1] & MASK4) << 2) + ((temp_a_3[2] & MASK5) >> 6);
temp_4[3] = temp_a_3[2] & MASK6;
for (j = i, k = 0; j < bufLen + 1 && j < i + 4; j++, k++)
encoded += base64_chars[temp_4[k]];
for (; k < 4; k++) encoded += EXTRA; // sentinal value
}
return encoded;
}
vector<UC> decode_base64(string const& encoded) {
UI i = 0, j = 0, k = 0, in_len = encoded.size();
UC temp_a_3[3], temp_4[4];
vector<UC> decoded;
for (i = 0; i < in_len; i += 4) {
for (j = i, k = 0; j < i + 4 && encoded[j] != EXTRA; j++)
temp_4[k++] = base64_chars.find(encoded[j]);
for (; k < 4; k++) temp_4[k++] = '\0';
temp_a_3[0] = (temp_4[0] << 2) + ((temp_4[1] & MASK7) >> 4);
temp_a_3[1] = ((temp_4[1] & MASK4) << 4) + ((temp_4[2] & MASK8) >> 2);
temp_a_3[2] = ((temp_4[2] & MASK2) << 6) + temp_4[3];
for (j = i, k = 0; k < 3 && encoded[j + 1] != EXTRA; j++, k++)
decoded.push_back(temp_a_3[k]);
}
return decoded;
}
int main() {
vector<UC> myData = {'6', '7', '8', '9'};
string encoded = encode_base64(&myData[0], myData.size());
cout << "Encoded String: " << encoded << '\n';
vector<UC> decoded = decode_base64(encoded);
cout << "Decoded Data: ";
for (int i = 0; i < decoded.size(); i++) cout << (char)decoded[i] << ' ';
cout << '\n';
myData = {4, 16, 64};
encoded = encode_base64(&myData[0], myData.size());
cout << "Encoded String: " << encoded << '\n';
decoded = decode_base64(encoded);
cout << "Decoded Data: ";
for (int i = 0; i < decoded.size(); i++) cout << (int)decoded[i] << ' ';
cout << '\n';
return 0;
}
主に、2つのデータセットがあります。 最初のセットには数字があります。 次のセットには数値があります。 したがって、最後のループでは、型を整数にキャストしてデコードされたメッセージを出力します。
出力:
Encoded String: Njc4OQ^^
Decoded Data: 6 7 8 9
Encoded String: BBBA
Decoded Data: 4 16 64
最初のセットは 4 文字 (4 バイト) で、エンコードされたメッセージ Njc4OQ^^
は 6 文字です (最後の 2 文字は余分です)。 2 番目のセットには 3 バイトがあり、エンコードされたメッセージ BBBA
には 4 バイトがあります。
base_64
エンコーディングでは、すべての文字に 1 に設定された最大 6 ビットがあり、対応する 64 個の base_64
プライマリ文字があります。 同様に、エンコードされたメッセージには、ASCII よりも 33% 多くのストレージが必要です。
追加のストレージにもかかわらず、利点は特殊文字を管理できることです。 したがって、このエンコード スキームはデータを転送し、完全性を維持します。