nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

base64の変換を作る

C言語Common Lispで、base64を扱うコードを作成しました。

https://github.com/nptcl/hypd

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は、改行を無視します。
具体的には文字コード0x0A0x0Dを無視します。
ignore_eol0のときに 改行コードが読み込まれるとエラーです。

ignore_othersは、異常な文字を無視します。
つまりbase64で使われる65文字と改行コード2種類の 計67文字以外が現れたとき、 本来であればエラーになるのですが無視するように指示します。

ignore_paddingは、最後に付与されるパディング文字を無視します。
パディング文字を完全に無視するのではなく、 本来あり得ない場所に出現していたり、 あるいは最後4文字に区切られていない場合はエラーになります。

4. Common Lispbase64エンコード

それでは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 LispC言語と違って クロージャーやらなにやら便利機能が使えますので、 上記のdo式は次のように書き直すこともできます。

(base64-encode-close
  encode
  (lambda (v) (push v value)))

それでは結果を見てみましょう

(setq value (nreverse value))
(format t "~S~%" (coerce value 'string))

結果は下記の通り。

"QUI="

5. Common Lispbase64のデコード

それではどんどん行きます。

(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の処理を好きなように組み込むことができると思います。