続いてバイナリデータをASCIIに変換して表示する処理を作っていきたいと思います。
そんな処理は当然Rustにあるだろうと思っていたのですが、執筆時点でstd::ascii::CharがExperimentalだったり、std::ascii::AsciiExtがDeprecatedだったり、as_ascii()がnightlyだったりしています。
意外とASCIIだけを扱う仕組みが整備されていない、というよりUnicodeのみの世界になっているようです。
as_ascii()は将来使えそうですが、標準ライブラリのみだとなかなか変換は厳しいかもしれないので外部クレートとしてasciiクレート、encoding_rsクレートを試して変換具合が良さそうなものを使いたいと思います。
外部クレートの導入
外部クレートの導入は通常と同じやり方でできますが、まだ正式採用では無いので開発用のクレートとして入れたいと思います。
開発用とする場合は、--dev
オプションを追加します。
[package]
name = "binllion"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crossterm = "0.28.1"
ratatui = "0.28.1"
$ cargo add --dev ascii
Updating crates.io index
Adding ascii v1.1.0 to dev-dependencies
Features:
+ alloc
+ std
- serde
- serde_test
Updating crates.io index
Blocking waiting for file lock on package cache
Locking 1 package to latest compatible version
Adding ascii v1.1.0
$ cargo add --dev encoding_rs
Updating crates.io index
Adding encoding_rs v0.8.34 to dev-dependencies
Features:
+ alloc
- any_all_workaround
- fast-big5-hanzi-encode
- fast-gb-hanzi-encode
- fast-hangul-encode
- fast-hanja-encode
- fast-kanji-encode
- fast-legacy-encode
- less-slow-big5-hanzi-encode
- less-slow-gb-hanzi-encode
- less-slow-kanji-encode
- serde
- simd-accel
Updating crates.io index
Blocking waiting for file lock on package cache
Locking 1 package to latest compatible version
Adding encoding_rs v0.8.34
[package]
name = "binllion"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crossterm = "0.28.1"
ratatui = "0.28.1"
[dev-dependencies]
ascii = "1.1.0"
encoding_rs = "0.8.34"
nightlyの導入
nightlyの機能を使うにはnightlyのRustを導入する必要があります。
nightlyなRustの導入自体は難しくありません。通常のRustの導入と同じようなやり方で導入できます。
具体的には、
rustup toolchain uninstall nightly-2024-09-30
今回はexamples/convert_to_ascii/にお試しコードを置いてこのコードのみにnightlyを機能を実行したかったので別の方法でやってみます。
rust-toolchain.tomlを同じフォルダに置くとディレクトリ単位で実行されるRustのバージョンを切り替えることができます。
このtomlを下記のように書いてフォルダに置いておくと、cargo runなどすると自動的に指定のnightlyなRustで実行してくれます。
まだnightlyがインストールされていない場合は、自動的にダウンロードしてくれるので便利です。
[toolchain]
channel = "nightly-2024-09-30"
profile = "minimal"
注意点としてはフォルダ単位に有効化されるのでカレントフォルダが別のフォルダだとstableなRustに切り替わってしまいます。
pwdコマンドなどで確認して実行しましょう。
VSCodeはそこまで認識してくれないようなのでツール上から実行するとうまくいかないようです。
testを試す
crosstermの処理で少し使ったのですが、cargo testの機能を試して見たいと思います。
Rustは単体テストや結合テストをする機能を持っています。
#[test]のアトリビュートを持つ関数がテスト用の関数として扱われます。テスト用の関数はmod testの中に定義され、コマンドラインからcargo testと実行するとテスト関数が実行されます。
ということで、導入したクレートなどを使い、ASCII変換処理を数パターン書いてみました。
コードの一部は下記になります。
// convert_to_ascii
// char::from(u8)
pub(crate) fn to_ascii_1(buf: &[u8]) -> String {
let res = buf.iter().fold(String::new(), |acc, x| format!("{}{}", acc, char::from(*x)));
res
}
// as_ascii
pub(crate) fn to_ascii_2(buf: &[u8]) -> String {
let res = buf.iter().fold(String::new(), |acc, x| format!("{}{}", acc, x.as_ascii().unwrap_or(std::ascii::Char::FullStop)));
res
}
// encoding_rs #1
use encoding_rs::mem::decode_latin1;
pub(crate) fn to_ascii_3(buf: &[u8]) -> String {
let res = decode_latin1(&buf);
res.into()
}
// encoding_rs #2
use encoding_rs::UTF_8;
pub(crate) fn to_ascii_4(buf: &[u8]) -> String {
let (res, _, _) = UTF_8.decode(&buf);
res.into()
}
// ascii crate #1
use ascii::{AsciiChar, AsciiStr, IntoAsciiString};
pub(crate) fn to_ascii_5(buf: &[u8]) -> String {
// let ascii_data = data.clone().into_ascii_string().unwrap_or_default();
let res = &buf.into_ascii_string().unwrap_or(AsciiChar::Dot.into());
res.to_string()
}
// ascii crate #2
pub(crate) fn to_ascii_6(buf: &[u8]) -> String {
let res: String = buf.iter().map(|x| AsciiChar::from_ascii(*x).unwrap_or(AsciiChar::Dot).as_printable_char()).collect();
res
}
// ascii crate #3
pub(crate) fn to_ascii_7(buf: &[u8]) -> String {
let res = buf.iter().fold(String::new(), |mut acc, &x| {acc.push(AsciiChar::from_ascii(x).unwrap_or(AsciiChar::Dot).as_printable_char());acc});
res
}
// ascii crate #4
pub(crate) fn to_ascii_8(buf: &[u8]) -> String {
let res = unsafe { AsciiStr::from_ascii_unchecked(&buf) };
res.to_string()
}
// Original #1
pub(crate) fn to_ascii_9(buf: &[u8]) -> String {
let res: String = buf.iter().map(|&x| format!("{}", ToPrintableAscii::as_printable_char(x))).collect();
res
}
struct ToPrintableAscii;
impl ToPrintableAscii {
pub fn as_printable_char(num: u8) -> char {
const DUMMY_CHAR: char = '.';
match num as u8 {
0x0..=0x1f => DUMMY_CHAR,
0x7f.. => DUMMY_CHAR,
_ => char::from(num),
}
}
}
#[cfg(test)]
mod test {
use crate::*;
fn setup()-> &'static [u8] {
let bin_data: &[u8]= &[
0x01, 0x02, 0x03, 0x00, 0x63, 0x71, 0x00, 0x61, 0x62, 0x0f, 0x01, 0x02, 0x03, 0x00, 0x63,
0x71, 0x0f, 0x61, 0x62, 0x63, 0x01, 0xff, 0x03, 0x00, 0x63, 0x71, 0x0f, 0x61, 0x62, 0x63,
];
bin_data
}
// 略
#[test]
fn test_to_ascii_1() {
// char::from(u8)
let bin_data = setup();
let res = to_ascii_1(bin_data);
dbg!(&res);
println!("{}: {}", "to_ascii_1", res);
}
#[test]
fn test_to_ascii_2() {
// as_ascii
let bin_data = setup();
let res = to_ascii_2(bin_data);
dbg!(&res);
println!("{}: {}", "to_ascii_2", res);
}
// 略
.....
テストの実行結果は下記のようになりました。
コードを書き進めてわかったのですが、標準出力やエラー出力に出てくるデータ量が多いとcargo testしても実行結果にdbg!やprintlen!の表示がされなくなります。
どうやらassert!などのマクロを使うのが前提のようです。
また、テストの実行はパラレルに行われるようです。
running 10 tests
test test::test_to_ascii_2 ... ok
test test::test_to_ascii_1 ... ok
test test::test_to_ascii_3 ... ok
test test::test_to_ascii_4 ... ok
test test::test_to_ascii_5 ... ok
test test::test_to_ascii_7 ... ok
test test::test_to_ascii_6 ... ok
test test::test_to_hex ... ok
test test::test_to_ascii_9 ... ok
test test::test_to_ascii_8 ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
出力も表示させたい場合は、下記のようにオプションを指定します。
-- --nocapture
とするとプリントアウトが表示されます。
cargo test --example convert_to_ascii -- --nocapture
テスト結果を確認する
見やすいように出力を下記のようにまとめました。
to_ascii_1: cqabcqabcÿcqabc
to_ascii_2: cqabcqabc.cqabc
to_ascii_3: cqabcqabcÿcqabc
to_ascii_4: cqabcqabc�cqabc
to_ascii_5: .
to_ascii_6: ␁␂␃␀cq␀ab␏␁␂␃␀cq␏abc␁.␃␀cq␏abc
to_ascii_7: ␁␂␃␀cq␀ab␏␁␂␃␀cq␏abc␁.␃␀cq␏abc
to_ascii_8: cqabcqabc�cqabc
to_ascii_9: ....cq.ab.....cq.abc....cq.abc
to_ascii_1はchar::from()を使って、u8をcharに変換するだけなので基本的にUnicodeとして扱われます。
to_ascii_2はas_asscii()を使っています。0xFFの変換に失敗した場合はドットを入れるようにしています。
to_ascii_3とto_ascii_4はencoding_rsクレートで変換しています。前者がdecode_latin1()、後者がUTF_8.decode()を使っています。to_ascii_1とto_ascii_4が同じになるかと思っていましたがto_ascii_3と同じになりました。
これまでの結果だと制御文字は切り捨ててアルファベット等文字として読めるものだけ表示する動作になっているようです。
これ以降はasciiクレートの処理です。
to_ascii_5はasciiクレートの一括変換する関数を呼び出したものです。変換に失敗しています。おそらく0xFFがASCIIコードの範囲外の為でしょう
to_ascii_6とto_ascii_7はas_printable_char()という目的に一番合致していそうな関数を使用したケースです。イテレータ処理を若干変えただけなので同じ出力になります。
to_ascii_8はfrom_ascii_uncheckedというunsafeな関数のケースです。あまりunsafeな機能は使いたくないのでお試しだけです。to_ascii_4と同じですね。
制御文字はUnicode変換してくれるas_printable_char()が当初は良いかと思っていたのですが、文字が見づらいしフォントが対応していないとおかしな表示になりそうです。
そこでas_printable_char()から着想を得て、to_ascii_9(ToPrintableAsciiを定義)を独自に書いてみました。制御文字等見えないものはドットに置き換えます。
外部クレートを使いたかったですが、to_ascii_9のケースで作っていきたいと思います。
TOC
GitHubにコードをアップロードしています。
コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。
# Example
$ cargo run --examples first_step