base64の変換を作る
C言語とCommon Lispで、base64を扱うコードを作成しました。
base64のコードはすでに何人も作成しており、
C言語では検索するだけでコードがいっぱい出てきます。
Common Lispだとcl-base64
あたりが有名かと思います。
それにもかかわらず新しく作成したのは
入力と出力をパイプ処理のように扱いたかったためです。
まあ本当は単に作りたかっただけですけど。
1. base64の入出力
base64とは、バイナリをテキストファイルに変換する機能です。
入力は普通のバイナリデータなので8bitの列です。
それに対して出力はテキストファイルであり、
内部では6bitの列をアルファベットなどに割り当てて表現します。
base64の変換処理である、エンコードを考えましょう。
まずエンコード処理は、入力の8bitの列を6bitの列に変換します。
変換といっても、ただ8bitに並んだデータを
無理やり6bit間隔に分けるだけです。
出力は6bitのデータを1byteで表現するので、
当然入力より出力の方が大きくなります。
つまり入力1byteに対して、出力は1byteか2byteになります。
これが本ライブラリで重要なところです。
次にbase64でエンコードされたテキストを、
元のバイナリファイルに戻すデコード処理を考えます。
デコードは、エンコードのときの事情と逆です。
つまり入力1byteに対して、出力は0byte(なし)か1byteになります。
まとめますと、
- エンコードは入力1byteあたり、出力1byteか2byte
- デコードは入力1byteあたり、出力0byteか1byte
これを頭に入れておいて、base64の関数を使ってみましょう。
2. C言語でbase64のエンコード
まずはC言語でやっていきます。
Common Lispはそのあとです。
C言語で必要なファイルは次の2つです。
エンコードとは、バイナリをテキストに変換する処理です。
初期化処理をしましょう。
struct base64_encode encode; base64_encode_init(&encode);
変数encode
を、関数base64_encode_init
で初期化しています。
なお初期化処理に対応する解放関数はありません。
適当に終わらせてもリークは発生しないのでご安心ください。
その次に、必要であれば62文字、63文字、パディングの文字を設定をしてください。
デフォルトでは次のような設定がされています。
encode.char_62 = '+'; encode.char_63 = '/'; encode.char_padding = '=';
パティングを最後に付与するかどうかの設定もできます。
標準ではパティングが出力されるので、
次のように設定されています。
encode.padding = 1;
以上で初期化と設定は完了です。
さっそくエンコードしてみましょう。
例としてAB
という2byteをbase64で変換してみます。
まずは最初のA
から。
char x, y; base64_encode_pipe(&encode, 'A', &x, &y);
エンコードは入力A
という1byteに対して、
出力x
の1byteか、あるいはy
も含む2byteが返却されます。
次のようにして出力しましょう。
if (x) printf("%c", x); if (y) printf("%c", y);
それでは、次の文字のB
を渡します。
base64_encode_pipe(&encode, 'B', &x, &y); if (x) printf("%c", x); if (y) printf("%c", y);
入力はこれで終わりですが、
まだ出力されていない文字があるかもしれません。
関数base_encode_closing
を使い、
全ての文字を出力しましょう。
for (;;) { base64_encode_closing(&encode, &x); if (x == 0) break; printf("%c", x); } printf("\n");
これでエンコード完了です。
実行結果は下記の通り。
QUI=
3. C言語でbase64のデコード
デコードとは、base64のテキストを元のバイナリに戻す処理です。
使い方はエンコードとほぼ同じですが、
デコードの場合は入力エラーが起こる可能性があるので、判定する必要があります。
つまりエンコードとは違って、変な入力が突っ込まれるかもしれないのです。
まずは初期化を行います。
struct base64_decode decode; base64_decode_init(&decode);
エンコードと同様、解放関数はないのでリークは発生しません。
文字の設定のデフォルト値は次の通り。
decode.char_62 = '+'; decode.char_63 = '/'; decode.char_padding = '=';
その他の設定もありますが、あとでまとめて説明します。
例として、エンコードの例で出力された
QUI=
をデコードしてみます。
まずはQ
から。
uint8_t x; int check; check = base64_decode_pipe(&decode, 'Q', &x);
check
には実行結果が、
x
には変換したデータが入ります。
まずはエラーチェックを行う必要があります。
if (check < 0) { fprintf(stderr, "decode error\n"); exit(1); }
上記のようにエラーが発生した場合は、
終了させるなど適切な処理を行ってください。
例ではexit(1)
でプロセスを強制終了させています。
デコードの出力は、0byteか1byteなので、
必ずx
を出力すればいいわけではありません。
次のようにcheck
を確認してください。
if (check) printf("%c", (int)x);
それでは2文字目のU
を出力します。
check = base64_decode_pipe(&decode, 'U', &x); if (check < 0) { fprintf(stderr, "decode error\n"); exit(1); } if (check) printf("%c", (int)x);
3文字目I
と4文字目=
も行います。
check = base64_decode_pipe(&decode, 'I', &x); (省略) check = base64_decode_pipe(&decode, '=', &x); (省略)
入力はこれで終わりですので、
関数base_decode_close
で終了します。
if (base64_decode_close(&decode)) { fprintf(stderr, "decode_close error\n"); exit(1); } printf("\n");
これでデコード処理は完了です。
実行すればAB
が出力されるはずです。
最後にオプションを説明します。
関数base64_decode_init
で初期化した直後には、
次の設定を行うことができます。
struct base64_decode decode; base64_decode_init(&decode); decode.ignore_eol = 1; decode.ignore_others = 0; decode.ignore_padding = 0;
ignore_eol
は、改行を無視します。
具体的には文字コード0x0A
と0x0D
を無視します。
ignore_eol
が0
のときに
改行コードが読み込まれるとエラーです。
ignore_others
は、異常な文字を無視します。
つまりbase64で使われる65文字と改行コード2種類の
計67文字以外が現れたとき、
本来であればエラーになるのですが無視するように指示します。
ignore_padding
は、最後に付与されるパディング文字を無視します。
パディング文字を完全に無視するのではなく、
本来あり得ない場所に出現していたり、
あるいは最後4文字に区切られていない場合はエラーになります。
4. Common Lispでbase64のエンコード
それではCommon Lispでやってみましょう。
必要なファイルは次の通り。
まずは読み込みます。
* (load #p"base64.lisp")
hypd-base64
というパッケージができるので、使えるようにします。
(defpackage work (:use cl hypd-base64)) (in-package work)
それではAB
という文字をエンコードしてみます。
まずは構造体の作成から。
(setq encode (base64-encode-init))
C言語のときと同様、オプションを設定できます。
デフォルトは次のようになります。
(setf (base64-encode-char-62 encode) #\+) (setf (base64-encode-char-63 encode) #\/) (setf (base64-encode-char-padding encode) #\=) (setf (base64-encode-padding encode) t)
最初の文字A
を入力に渡します。
(setq v (char-code #\A)) (multiple-value-setq (x y) (base64-encode-pipe encode v))
関数base64-encode-pipe
の入力には文字を指定できないため、
変数v
に整数を代入してから渡しています。
関数の返却値x
, y
にはnil
か文字が入っているため、
その結果を取り出しましょう。
ただし、C言語と違ってインタラクティブで実行している場合は、
出力しても意味が分からなくなるため、
結果を格納する変数を新たに用意することにします。
(setq value nil)
上記の変数value
に結果を入れていきましょう。
(when x (push x value)) (when y (push y value))
次はB
を入力に渡します。
(setq v (char-code #\B)) (multiple-value-setq (x y) (base64-encode-pipe encode v)) (when x (push x value)) (when y (push y value))
入力が終わったらbase64-encode-closing
を行います。
(do (v) (nil) (setq v (base64-encode-closing encode)) (if v (push v value) (return nil)))
このclosing処理ですが、
Common LispはC言語と違って
クロージャーやらなにやら便利機能が使えますので、
上記のdo
式は次のように書き直すこともできます。
(base64-encode-close encode (lambda (v) (push v value)))
それでは結果を見てみましょう
(setq value (nreverse value)) (format t "~S~%" (coerce value 'string))
結果は下記の通り。
"QUI="
5. Common Lispでbase64のデコード
それではどんどん行きます。
(setq decode (base64-decode-init))
オプションのデフォルトは次のようになります。
(setf (base64-decode-char-62 decode) #\+) (setf (base64-decode-char-63 decode) #\/) (setf (base64-decode-char-padding decode) #\=) (setf (base64-decode-ignore-eol decode) t) (setf (base64-decode-ignore-others decode) nil) (setf (base64-decode-ignore-padding decode) nil)
最初のQ
を入力します。
(setq value nil) (let ((x (base64-decode-pipe decode #\Q))) (when x (push x value)))
入力を続けます。
(let ((x (base64-decode-pipe decode #\U))) (when x (push x value))) (let ((x (base64-decode-pipe decode #\I))) (when x (push x value))) (let ((x (base64-decode-pipe decode #\=))) (when x (push x value)))
クローズ処理を行います。
(base64-decode-close inst)
それでは結果を見てみましょう
(setq value (nreverse value)) (format t "~S~%" (coerce value 'string))
結果は下記の通り。
"AB"
6. Common Lispで配列を使う
Common Lispにて、入力と出力に配列を使う例を示します。
配列の要素の型は、微妙に異なるので注意してください。
エンコードは、入力がバイナリで出力が文字列です。
デコードは、入力が文字列で出力がバイナリです。
テストしやすくするために、相互変換できる便利な関数を用意しましょう。
(defun coerce-binary (str) (map 'vector (lambda (c) (if (characterp c) (char-code c) c)) str)) (defun coerce-string (str) (map 'string (lambda (c) (if (characterp c) c (code-char c))) str))
試しに実行してみます。
* (coerce-binary "Hello") -> #(72 101 108 108 111) * (coerce-string #(72 101 108 108 111)) -> "Hello"
まずはエンコードする関数を作成します。
(defun base64-encode-binary (input &optional (inst (base64-encode-init))) (with-output-to-string (s) (dotimes (i (length input)) (let ((v (elt input i))) (multiple-value-bind (x y) (base64-encode-pipe inst v) (when x (write-char x s)) (when y (write-char y s))))) (base64-encode-close inst (lambda (v) (write-char v s)))))
実行してみましょう。
* (base64-encode-binary (coerce-binary "ABC")) -> "QUJD"
base64に変換されているのが分かります。
配列から配列へ変換するというものは、
使いやすいかどうかはともかく、
わかりやすいとは思います。
それではデコードの方を作成します。
デコードの場合は、返却値をためるときに使用した
with-output-to-string
のような便利な関数はないので、
vector-push-extend
で伸長する仕組みを作りました。
(defun base64-decode-string (input &optional (inst (base64-decode-init))) (let ((r (make-array 16 :adjustable t :fill-pointer 0 :element-type '(mod 256)))) (flet ((push-value (x) (vector-push-extend x r (array-total-size r)))) (dotimes (i (length input)) (let* ((v (elt input i)) (x (base64-decode-pipe inst v))) (when x (push-value x)))) (base64-decode-close inst) r)))
それでは実行してみます。
* (coerce-string (base64-decode-string "QUJD")) -> "ABC"
正しく変換されているのが分かります。
ここまで理解できれば、 base64の処理を好きなように組み込むことができると思います。