Base64 各位朋友應該都有聽過,也使用過,但是 Base32 或許就比較少了,而且 .NET Framework 也沒有內建 Base32 的 API,會碰到 Base32 是因為有一個需要編碼結果全小寫的需求,顯然 Base64 並不適合,.NET Framework 又沒有 Base32 可以用,那就只好自己來寫一個了。
Base32 編碼與解碼規則
Base32 的編碼規則是這樣的,將資料位元組一字排開後,每 5 個 bits 取一個樣,轉成十進制之後就會得到一個 0 ~ 31 之間的數字,再到一個 32 個字元的對照表中,以該數字為索引,找到相對應的字元,組成一串字串,即 Base32 的編碼結果。
舉例來說,ASCII 小寫 a 的二進位表達如下:
每 5 個 bits 取一個樣,就會拆成 2 個 bytes,不足 5 個 bits 的部分就往後補 0。
我們就會得到兩個數字 12 跟 4,再從 Base32 標準的對照表中分別找到索引 12 跟 4 的字元,就是 M 跟 E,所以 ASCII 小寫 a 的 Base32 編碼就是「ME」,但是 RFC 4648 有規定,編碼結果的字元個數不足 8 的倍數,必須補填充字元到個數為 8 的倍數,填充字元也有出現在對照表中,就是「=」,因此最終編碼結果就會變成「ME======」。
解碼就把它反回去而已,我們收到「ME======」編碼後的內容,知道填充字元沒有意義,去掉之後剩下「ME」,從 Base32 標準對照表可以取得 12 跟 4 這兩個數字,轉成二進位表達之後就變這樣。
然後就把它兜回去,尾數超過 1 個 byte 長度的部分則去掉。
最後再拿這個 byte 做 ASCII 編碼,就會得到小寫 a,以上就是 Base32 編碼及解碼的規則,其實 Base64 也是一樣的做法,另外還有不同於取 bits 算法的編碼,像是 Base36、Base58,有興趣的朋友可以參考一下。
>>(右移)和 <<(左移)運算子
好了,我們知道規則之後,就是要來實作它了,我們無法對 byte 這個值型別的內容直接 5 bits、5 bits 這樣拆解,我們可以動用 >>(右移)和 <<(左移)運算子。
直接用範例解說,如果我將 3 >> 1 會等於 1,我將 2 >> 1 也會等於 1,請看下圖:
就是在以二進位表達的狀態之下,往右平移 1 個位置,出界的部分把它去掉,反之,左移亦然,以 ASCII 小寫 a 為例,預計編碼結果會需要 2 個 bytes,第 1 個 byte 透過 >> 3 取得。
第 2 個 byte 先 << 5,再 >> 3 取得。
最後拿這 2 個 bytes 到對照表查詢對應的字元,組成一串字串就完成了,無論我們處理的 byte 長度為何,只要照這個邏輯一一拆解,最終都可以完成編碼,解碼就反過來一一組合回去,原始碼我貼在下面給各位參考,有一件事我們要注意一下就是編碼之後的大小,會比原本還要膨脹 60%,而 Base64 則是膨脹 33%,這一點我們需要自己衡量一下。
public static class Base32
{
private static readonly string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
public static string Encode(string value, Encoding encoding)
{
if (string.IsNullOrEmpty(value)) return null;
var valueBytes = encoding.GetBytes(value);
var encodedBuilder = new StringBuilder();
var position = 0;
var left = 0;
for (var i = 0; i < valueBytes.Length * 8 / 5 + (valueBytes.Length * 8 % 5 == 0 ? 0 : 1); i++)
{
var encodedByte = default(byte);
if (left > 0)
{
encodedByte |= (byte)(valueBytes[position] << (8 - left));
if (left <= 5 && position < valueBytes.Length - 1)
{
position++;
if (left < 5) encodedByte |= (byte)(valueBytes[position] >> left);
}
}
else
{
encodedByte |= valueBytes[position];
}
encodedBuilder.Append(Alphabet[(byte)(encodedByte >> 3)]);
left = 8 * (position + 1) - 5 * (i + 1);
}
encodedBuilder.Append(new string('=', encodedBuilder.Length % 8));
return encodedBuilder.ToString();
}
public static string Decode(string value, Encoding encoding)
{
if (string.IsNullOrEmpty(value)) return null;
value = value.ToUpper().TrimEnd('=');
var decodedBytes = new byte[value.Length * 5 / 8];
var position = 0;
var available = 0;
for (var i = 0; i < value.Length; i++)
{
var symbol = (byte)(Alphabet.IndexOf(value[i]) << 3);
if (available > 0)
{
decodedBytes[position] |= (byte)(symbol >> (8 - available));
if (available <= 5 && position < decodedBytes.Length - 1)
{
decodedBytes[++position] |= (byte)(symbol << available);
}
}
else
{
decodedBytes[position] |= symbol;
}
available = 8 * (position + 1) - 5 * (i + 1);
}
return encoding.GetString(decodedBytes);
}
}