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のこのコードはちょっと見づらく感じるかもしれません。少しずつ見ていきましょう。

この記事は
Members+会員の方のみ御覧いただけます

ログイン/無料会員登録

会員サービスの詳細はこちら