Base64 の目的
8bitで表現される任意の値が設定されたバイト列を、安全に通信するためにテキストで表現できる形式に変換すること。
Base64 encode の変換形式
8bitのバイト列を6bitずつ取り出し、[a-zA-Z0-9+/] に割り当て直す。
つまり、24 bit = 8 bit * 3 のバイト列は 24 bit = 6 bit * 4 のバイト列に変換される
変換の概要
8bit の列を 6bit の列に分割する
bbbbbbbb bbbbbbbb bbbbbbbb
↓
bbbbbb bbbbbb bbbbbb bbbbbb
6bit の列を定義した変換表を使って変換する
0b000001 = 'A'
0b000010 = 'B'
0b000011 = 'C'
...
0b111111 = '/'
3バイトに満たない列には'='を埋める
Base64 の問題点
割り当て直す [a-zA-Z0-9+/] のうち、[a-zA-Z0-9] はどういう環境でも安全に扱えるが、のこりの [+/] については環境によってはそのまま使えず別途エンコードする必要がある。たとえば '/' はパス区切りには使えず、URL表現だと問題が発生するし、'+' も同様にURL表現で問題が発生する可能性がある。
ゆえに[+/]を別の安全に使えそうな文字に置き換えるとかする必要がある(典型的にはURLEncodeによって数値参照形式にするなど)
C#による実装
似たロジックはSMF(Standard MiIDI Format)における8bitの列から7bitずつを取り出す方法だが、SMFだとC#のバイト列とはエンディアンが逆なのでそのままは使えない。
参考: https://qiita.com/ringorou/items/5e2384f7cf226d9e648a
考え方
変換対象のバイト列の長さは不定だが、8bit から 6bit ずつ取り出せばよいので 24bit のみ考えればよい。
1.第1バイトを2ビット右シフトした値と 0b111111 の積 a を格納
2. 第1バイトを4ビット左シフトした値と 0b111111 の積 b を格納
3. 第2バイトを4ビット右シフトした値とbの和を 0b111111 の積をとった値 c を格納
4. 第2バイトを2ビット左シフトした値と 0b111111 の積を d を格納
5. 第3バイトを6ビット右シフトした値とdの和を 0b111111 の積をとった値 e を格納
6. 第3バイトと 0b111111の積 f を格納
7. 格納した値 a, c, e, f の列を変換表を使って変換する
8. 余った位置には'=' を追加する
実装
// 変換表
static readonly string map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
public static string Encode(byte[] source)
{
// エンコード後の長さを計算し、余る長さを求める
var encodedLength = source.Length * 8 / 6;
var remain = 0;
if ((source.Length * 8 % 6) > 0)
{
encodedLength++;
remain = (4 - encodedLength % 4);
encodedLength += remain;
}
var encoded = new char[encodedLength];
int offset = 0;
byte next = 0;
var n = 0;
var m = 0;
int i;
// 変換の本体
while (n < source.Length)
{
next = 0;
for (i = 0; i < 3 && n < source.Length; ++i, ++n)
{
offset = (i + 1) * 2;
encoded[m++] = map[(next | (source[n] >> offset)) & 0b111111];
next = (byte)((source[n] << (6 - offset)) & 0b111111);
}
encoded[m++] = map[next];
}
if (remain > 0)
{
for (var p = encodedLength - remain; p < encodedLength; ++p)
{
encoded[p] = '=';
}
}
return new string(encoded);
}
その他
- 当然、組み込みの関数を使った方が効率はよい。
- 例えば '+','/' を別の文字に置き換える場合でも組み込み関数の出力を Replace等で置き換えた方が早い。
- 上のロジックでnew string()するのはいかがなもの、と思うがStringBuilderに変換結果を入れても最終的には同じことになるので意味はない
- 他に高速化、効率化するポイントはあると思うけどとりあえずの目的は達成できる
以下はそれぞれ Convert.ToBase64String, それの'+/'をReplaceで変換するもの、この記事のロジックでのベンチマーク結果。
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
ConvertBase64 | 249.4 ns | 560.1 ns | 30.70 ns | 0.0758 | - | - | 120 B |
ConverBase64Replace | 310.4 ns | 404.5 ns | 22.17 ns | 0.1049 | - | - | 165 B |
OriginalBase64 | 543.8 ns | 982.8 ns | 53.87 ns | 0.1216 | - | - | 192 B |