Rustは実行効率や安全性を重視した人気のプログラミング言語ですが、難しいと言われることもあります。本連載ではいろいろな有名アルゴリズムを解くことでRustに慣れていきます。今回は、Base64のエンコーダーを実装してみましょう。

  • Base64エンコーダーを実装して実行したところ

    Base64エンコーダーを実装して実行したところ

Base64とは何か?

Base64とはバイナリデータを、64種類の英数字のみを用いて表現するエンコード方式です。バイナリデータをASCIIテキストとして扱えるので、電子メールの添付ファイルをはじめ、HTMLファイルの中に画像ファイルを埋め込むなど、いろいろな用途で利用されています。

Base64エンコーダーを作るという課題はRustでバイナリデータを扱う練習にもなりますので挑戦してみましょう。なお、こちらで、JavaScriptを用いてBase64エンコーダーを作る方法を紹介しています。Rustのプログラムと見比べてみると、二倍楽しめるでしょう。

Base64の仕組み

Base64は、基本的にアルファベット(大文字と小文字)と記号(+と-)の64文字でデータを表現します。ただし、パディング処理に記号「=」に使うため実際には65文字を用いてデータを表現します。それで、データ量はバイナリデータに比べて約1.3倍となります。

次のような手順でデータのエンコードを行います。

 (1) 文字列であればバイナリデータに変換しておく
 (2) データを2進数に変換し6ビットごとに分割する(この時、余った部分は0にする)
 (3) 変換表に従って各6ビットをBase64の文字に変換する
 (4) 変換後の文字列は必ず4文字ずつにする(足りない部分は"="で埋める)

上記手順の(3)で使う変換表ですが、0から63までの値は、A-Za-z0-9+/の順に並んだもので、次の通りの表です。

  • Base64の変換テーブル

    Base64の変換テーブル

以下は、文字列とBase64の変換例です。プログラムが完成したら正しく変換できるか確かめてみましょう。

変換プログラムを作ろう

以下が文字列をBase64にエンコードするプログラムです。コメントを含めてちょうど50行です。前述のJavaScript版が43行なので、少しRustの方が長くなりました。

念のためソースコードはこちらからダウンロードできるようにしています。以下のプログラムを「base64enc.rs」という名前で保存しましょう。

// Base64のエンコード処理を作る
fn main() { // 適当な文字列をBase64に変換して結果を表示 --- (*1)
    let s = "hello!";
    println!("{} => {}", s, base64_encode(s));
    let s = "Rust";
    println!("{} => {}", s, base64_encode(s));
    let s = "生姜焼き定食";
    println!("{} => {}", s, base64_encode(s));
}
// Base64エンコードを行う関数 --- (*2)
fn base64_encode(in_str: &str) -> String {
    // Base64の変換テーブルを1文字ずつに区切る --- (*3)
    let t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let table: Vec<char> = t.chars().collect::<Vec<char>>();
    // 変換結果を保持する文字列 --- (*4)
    let mut result = String::new();
    // 入力文字列をバイト列に変換 --- (*5)
    let bin8 = in_str.as_bytes();
    // 繰り返し24bitごと(3文字ずつ)に処理する --- (*6)
    let cnt = bin8.len() / 3;
    for i in 0..cnt {
        let n = i * 3; // 3文字(24bit)ずつ処理 --- (*7)
        let b24 = ((bin8[n+0] as usize) << 16) +
                  ((bin8[n+1] as usize) <<  8) +
                  ((bin8[n+2] as usize) <<  0);
        result.push(table[(b24 >> 18) & 0x3f]); // 6bitずつ変換 --- (*8)
        result.push(table[(b24 >> 12) & 0x3f]);
        result.push(table[(b24 >>  6) & 0x3f]);
        result.push(table[(b24 >>  0) & 0x3f]);
    }
    // 3バイトずつに割り切れなかった余りの部分を処理 --- (*9)
    match bin8.len() % 3 {
        1 => {
            let b24 = (bin8[cnt*3] as usize) << 16;
            result.push(table[(b24 >> 18) & 0x3f]);
            result.push(table[(b24 >> 12) & 0x3f]);
            result.push_str("==");
        },
        2 => {
            let b24 = ((bin8[cnt*3+0] as usize) << 16) +
                      ((bin8[cnt*3+1] as usize) << 8);
            result.push(table[(b24 >> 18) & 0x3f]);
            result.push(table[(b24 >> 12) & 0x3f]);
            result.push(table[(b24 >>  6) & 0x3f]);
            result.push('=');
        },
        _ => {},
    }
    result
}

プログラムをコンパイルして実行するには、ターミナルで以下のコマンドを実行します。ここでは「hello! 」と「Rust」と「生姜焼き定食」の3つの文字列をBase64に変換して表示します。

$ rustc base64enc.rs && ./base64enc
hello! => aGVsbG8h
Rust => UnVzdA==
生姜焼き定食 => 55Sf5aec54S844GN5a6a6aOf

正しくコンパイルできると次のように表示されます。

  • 文字列をBase64に変換したところ

    文字列をBase64に変換したところ

プログラムを確認してみましょう。なお、Base64では、8ビットのデータを6ビットごとに分けて変換するという処理通り、ビット操作が多く登場します。スクリプト言語に慣れていると、コンパイラ言語のRustのこのコードはちょっと見づらく感じるかもしれません。少しずつ見ていきましょう。

(*1)ではmain関数にテストコードを記述しています。適当な文字列を3つBase64に変換して表示します。

(*2)ではBase64エンコードを行う関数base64_encodeを定義します。引数には文字列の参照(&str型)を指定し、戻り値はString型となります。

(*3)ではBase64の変換テーブルを指定します。なお、Rustでは文字列に対して任意の1文字を取り出すのが面倒なので、ここでは、可変配列であるVec型を利用して、変数tableを通して手軽に任意の1文字にアクセスできるようにします。

(*4)では変換結果を保持する文字列resultを初期化します。(*5)では入力文字列をバイト列(正しくは、&[u8] で、8ビット整数のスライス)に変換します。

(*6)以降では、入力データを24ビット(3文字ずつ)処理します。24ビットというのがポイントです。入力データを3バイト(8ビット×3=24ビット)ずつ処理すると、出力データのBase64では4文字(6ビット×4=24ビット)ずつ出力できます。

具体的には、(*7)で入力データを3バイト(8ビット×3=24ビット)のデータを作成し、(*8)で6ビットずつ4文字(6ビット×4=24ビット)に分割して出力します。

(*9)では、3バイトずつに割り切れなかった部分を処理します。同じように24ビットのデータに変換し、6ビットずつに分割するという処理を行います。そして、4文字ずつに足りない部分に"="を補完します。

まとめ

以上、今回はBase64のエンコードを行うプログラムを作ってみました。3バイト(24bit)入力して、それを6ビットずつに分割して4文字出力するという点がポイントでした。RustはC/C++言語の置き換えを狙っているだけあって、今回のようなビット操作主体の処理を記述する場合に、かなりスッキリと記述できました。参考にしてみてください。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。直近では、「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」「すぐに使える!業務で実践できる! PythonによるAI・機械学習・深層学習アプリのつくり方 TensorFlow2対応(ソシム)」「マンガでざっくり学ぶPython(マイナビ出版)」など。