[創意料理] 生平第一次使用 >>(右移)、<<(左移)運算子就獻給 Base32 了

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 算法的編碼,像是 Base36Base58,有興趣的朋友可以參考一下。

>>(右移)和 <<(左移)運算子

好了,我們知道規則之後,就是要來實作它了,我們無法對 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);
    }
}

 


   
  如果這篇文章值得你請我喝飲料
可以使用街口支付斗內我一杯