Rustは実行効率や安全性を重視した人気のプログラミング言語ですが、難しいと言われることもあります。本連載ではいろいろな有名アルゴリズムを解くことでRustに慣れていきます。今回は、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にエンコードするプログラムです。コメントを含めてちょうど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では、8ビットのデータを6ビットごとに分けて変換するという処理通り、ビット操作が多く登場します。スクリプト言語に慣れていると、コンパイラ言語のRustのこのコードはちょっと見づらく感じるかもしれません。少しずつ見ていきましょう。