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のこのコードはちょっと見づらく感じるかもしれません。少しずつ見ていきましょう。
(*1)ではmain関数にテストコードを記述しています。適当な文字列を3つBase64に変換して表示します。
(*2)ではBase64エンコードを行う関数base64_encodeを定義します。引数には文字列の参照(&str型)を指定し、戻り値はString型となります。
(*3)ではBase64の変換テーブルを指定します。なお、Rustでは文字列に対して任意の1文字を取り出すのが面倒なので、ここでは、可変配列であるVec
(*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(マイナビ出版)」など。