Quoted-Printable編碼原理及代碼實現

這篇文章是我之前在RYTong內部分享的一篇文章,摘取了有用的部分。當時幫助某項目郵件系統解決問題,期間瞭解到Quoted-Printable編碼,在此與大家分享下該編碼的原理和個人版本的代碼實現。

關於規範

關於Quoted-Printable的編碼規範,需要參考rfc2045

爲了方便大家閱讀,在此給大家看一下融合我個人理解的翻譯:

  1. 除了換行(CRLF序列)中的CR或LF,所有(8bit)字節都可以表示爲符號”=”後跟兩個16進制數字(16進制數字表示該字節的值)的格式,16進制數字只能有數字和大寫ABCDEF表示,不能用小寫字母。例如,ASCII碼換頁符,數值爲12,編碼後表示爲”=0C”。

  2. 值爲33到126的字節(除61外),包含邊界,也可以不進行編碼。

  3. 製表符(值爲9)和空格(值爲32)可以不進行編碼,但未編碼的兩者不能位於編碼後的行尾。如果行尾由軟換行符(符號”=”,參照規則5)時,軟換行符前的製表符和空格可以不編碼。兩者若在行尾則必須編碼。

  4. 換行(CRLF序列)不必進行編碼。因爲只有CRLF序列表示換行,因此包含CR或LF的其他序列須進行編碼。

  5. Quoted-Printable編碼要求編碼後的文本每行長度不得超過76字符。如果原文的一行被編碼後的文本長度超過76,那麼需要在編碼後的行尾添加軟換行符”=”,表示原文此處沒有換行。注意軟換行符”=”也會計入編碼後的字節長度,也就是說如果編碼後行尾存在軟換行符,那麼該行其他字符數不得超過75。

  6. 建議對-!\”#$@[\]^`{|}~進行編碼,以避免對其他協議或少數網關(如EBCDIC)的處理造成影響。

  7. 建議對CRLF序列進行編碼,避免各平臺不同的換行序列對解碼造成影響。

關於代碼實現

以下是本人基於以上規則實現的代碼(Erlang實現)

-module(qp_encoder).

-export([qp_encode/1]).

-define(LINE_LENGTH, 76).
-define(is_suggested(C),
        C == 45 orelse C == 64 orelse C == 96 orelse C == 46 orelse
            (C >= 33 andalso C =< 36) orelse
            (C >= 91 andalso C =< 94) orelse
            (C >= 123 andalso C =< 126)).

-define(CR, 13).
-define(LF, 10).
-define(HT, 9).
-define(SPACE, 32).

%%%%%%%%%%%%%%%%%%%%%%%%%% API %%%%%%%%%%%%%%%%%%%%%%%%%%

%% @spec (Input :: binary() | string()) -> Output :: binary()
qp_encode(String) when is_list(String) ->
    qp_encode(list_to_binary(String));
qp_encode(Bin) when is_binary(Bin) ->
    qp_encode(0, Bin, <<>>, <<>>, <<>>).

%% @spec (Input :: binary() | string()) -> Output :: binary()
qp_decode(String) when is_list(String) ->
    qp_decode(list_to_binary(String));
qp_decode(Bin) when is_binary(Bin) ->
    qp_decode(Bin, <<>>, 1, 1).



%%%%%%%%%%%%%%%%%%%%%%%%%% internal %%%%%%%%%%%%%%%%%%%%%%%%%%

%% Num is the total number of characters of LineAcc + LastBuf
%% Bin is the input binary to be encoded
%% LastBuf is the encoded binary of last character
%% LineAcc is the current line of encoded binary
%% Acc is the output binary
qp_encode(Num, Bin, LastBuf, LineAcc, Acc) when Num >= ?LINE_LENGTH ->
    {CurrentLine, LastBuf2, NextLineNum} = qp_new_line(LastBuf, LineAcc),
    qp_encode(NextLineNum, Bin, LastBuf2, <<>>,
              <<Acc/binary, CurrentLine/binary>>);
qp_encode(_, <<>>, LastBuf, LineAcc, Acc) ->
    Encoded =
        case LastBuf of
            <<C>> when C == ?HT orelse C == ?SPACE ->
                do_qp_encode(C);
            _ ->
                LastBuf
        end,
    <<Acc/binary, LineAcc/binary, Encoded/binary>>;
qp_encode(Num, <<C, Rest/binary>>, LastBuf, LineAcc, Acc) ->
    {EncNum, Enc} = inline_qp_encode(C),
    qp_encode(Num + EncNum, Rest, Enc,
              <<LineAcc/binary, LastBuf/binary>>, Acc).

%% nethier HT nor SPACE could be presented at the end of an encoded line
qp_new_line(<<C>>, CurrentLine) ->
    {<<CurrentLine/binary, $=, ?CR, ?LF>>, <<C>>, 1};
qp_new_line(LastBuf, CurrentLine) ->
    {<<CurrentLine/binary, $=, ?CR, ?LF>>, LastBuf, 3}.

%% characters(-!\"#$@[\]^`{|}~.) are suggested to be encoded
inline_qp_encode(C) when ?is_suggested(C) ->
    {3, do_qp_encode(C)};
%% character "=" must be encoded
inline_qp_encode($=) ->
    {3, do_qp_encode($=)};
%% character whose ascii code is between 33 and 126 inclusively need not
%% be encoded
inline_qp_encode(C) when C >= 33 andalso C =< 126 ->
    {1, <<C>>};
%% HT and SPACE need not be encoded
inline_qp_encode(C) when C == ?HT orelse C == ?SPACE ->
    {1, <<C>>};
%% others must be encoded
inline_qp_encode(C) ->
    {3, do_qp_encode(C)}.

do_qp_encode(Int) when is_integer(Int) ->
    [Hex1, Hex2] =
        case integer_to_list(Int, 16) of
            [H] ->
                [$0, H];
            _Val ->
                _Val
        end,
    <<$=, Hex1, Hex2>>.

第一條規則由do_qp_encode/1函數實現,這部分值得注意的是integer_to_list/2方法的使用,可以直接將一個整數轉成對應的16進制字符串。
第二條規則由67到70行的代碼實現,此處用了Erlang模式匹配的技巧,由於會優先匹配68行的函數,因此72行函數判斷條件並不需要將“=”號排除。
第三條規則由75和76行的代碼實現,大家可能會覺得此處邏輯存在問題,因爲編碼後行尾是不能存在未編碼的製表符(值爲9)和空格(值爲32)的。這裏是因爲我按規則7將CRLF進行了編碼,因此編碼後的每行都是以“=”結束的軟換行,所以我並未對製表符和空格進行編碼。由於最後一行的編碼內容是不存在軟換行的,因此我加了46到51行的代碼處理,將最後一行末尾出現的製表符和空格進行了編碼。
第四條規則和第七條有衝突,這裏我按第7條進行了實現。
第五條規則由40到43行代碼實現。
第六條規則由65和66行代碼實現。大家可以注意到,我在代碼邏輯裏增加了對“.”號的編碼,這是由於我們發現在實際使用(郵件轉發)中,發現部分出現在行首的“.”號會被其他服務器刪除掉(原因未知),因此特意對該符號進行了編碼,編碼之後此問題解決。

除了Quoted-Printable編碼規則外,大家還可以通過這個例子瞭解一下Erlang binary的相關語法,用起來還是很方便的。有興趣的同學可以先學習一下《Programming Erlang》這本書的5.2和5.3章節。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章