mini_markdown BUG 退散

BUG退治

前回 mini_markdown を使って、README.mdをhtmlに変換するやつに挑戦した。そしてBUGを見付けてしまった。正しく変換出来無いってやつと、もう一つは喰わせるファイルによっては無限ループになるってもの。

作者さんには、報告しといたんだけど、無視されている。

Subject: BUG in mini_markdown
-----------------------------
Hi Jon,

I am Sakae, 70 years old Japanese.
I have been studying rust since last month.
Thank you for publishing useful software.

I found a BUG where I used a mini-markdown,
so I will report it.
I don't have a git account, so I emailed it.

1. mini_markdown/README.md cannot be converted correctly.
2. It may hang. try ...
      serde_json-1.0.78/README.md
      cc-1.0.73/README.md

Although it is in Japanese, refer to the URL for details.

https://hamesspam.sakura.ne.jp/hes2022/220225.html

Best regards. 
Sakae

ならば、自分で何とかせいよって声が、どこからとなく聞こえてきた。途中で挫折するかも知れないけど、チャレンジ。まるで、ソフトウェア業界の、羽生結弦だな。

もうオリンピックは終ってしまったけど、 フィギアスケート業界の4回転半のジャンプって分かり易い表現。それに対してハーフパイプあたりでは、1260バックサイドとか、ピンとこないぞ。

何故3回転半と言わない。大きい数字の方が凄そうって言う誇大広告か。かっこよくしたいなら、7pi とか、数学の教養を曝け出すのがいいぞ。

lex出力

敵を知る亊が一番先だ。コードをざっと見したら、lex関数で解析して、要素を抽出してるんだなって亊が分った。何それ、単に関数のシグネチャを見ただけじゃん。ええ、それはhaskell譲りのお作法です。

dbg!(..)と言う、print文を挿入して、出力をモニターする。ソースは一番簡単なやつだ。

pub fn render(source: &str) -> String {
    parse(  dbg!(&lex(source)) )
}

rennderは、備付になってるけど、こんな亊もあろうかとmain.rsに引っ張り出しておいたのさ。まあ、責任分離点って亊です。

[src/main.rs:6] &lex(source) = [
    Header(
        1,
        "My first code",
        None,
    ),
    Plaintext(
        "\n",
    ),
    CodeBlock(
        "fn add(x: i32, y: i32) -> i32 { x + y }\n",
        "plaintext",
    ),
    Newline,
    Plaintext(
        "Is it ok?\n",
    ),
    Link(
        "https://choosealicense.com/licenses/mit/",
        Some(
            "MIT",
        ),
        None,
    ),
    Plaintext(
        "\n",
    ),
]

徹底的にpretty-printされてるけど、脳内変換してみれば、複数の関数が並んだ配列ってかVecになってるんだな。

こうしておけは、後はparse関数で、それを一つづつ取出して、html用のTAGを付けるだけでよい。と言う亊で、敵は本能寺に有りじゃなくて、lexに有りって亊が判明した。

loop or while

上で2つの問題点を指摘したけど、悪性なやつは、後者の無限ループになっちゃう方。実際に試用したら、それはもう常にロシアンルーレットに参加してる亊になりますから! オイラーは、そんなのイヤダ。

世の中には、こういう博打好きな人も大勢いますけどね。(株)大好きさんとか、パチンコ大好きさんとか、賭けマージャン大好きさんとか、おっと、競馬、競輪とかもその範疇に入るな。

このREADME.mdは悪性そうだけら止めようって分れば最高と思うぞ。で、何故ハングしちゃう? それは、多分loopとかwhile構文の中から脱出出来無いからでしょう。

ob$ grep -n loop lexer.rs
299:        loop {
ob$ grep -n while lexer.rs | wc
      38     353    3005

whileのオンパレードです。こんなのは、とても追跡出来ませんよ。はて、どうしたものか? 散歩してくるわ。

力技 or 脳技

永久ループを発生させてしまうREADME.mdをバイナリー分割して行って、ちゃんと動く部分を見きわめて行く。要は原稿の後半を削除して、それで動くか。動くなら、削除してしまった後半に問題が有るって亊になる。正に力技。

頭を使った技は、トークンの配列をモニターするって方法。lexerでトークンが検出される度にVecに追加されてくから、どこまで追加したらトークンの検出がおかしくなったか分るはず。

lex関数は、 mini_markdown.rs に有るので、そこに簡単なモニターを仕込む。

pub fn mon(tokens: &Vec<lexer::Token>, mut cnt: usize) -> () {
    if tokens.len() != cnt {
        dbg!(tokens);
        cnt += 1;
    }
}

/// Convert source markdown to an ordered vector of tokens
pub fn lex<'source>(source: &'source str) -> Vec<Token>{
    let mut char_iter = source.chars().peekable();
    let mut tokens = Vec::new();
    let  cnt: usize = 0;                         // add
    while char_iter.peek().is_some(){
        mon(&tokens, cnt);                       // add
        match char_iter.peek().unwrap(){
            '#' => {
             :

ループする題材は、cc-1.0.72/README.md 冒頭部分は、こうなっていた。

# cc-rs

A library to compile C/C++/assembly into a Rust library/application.

[Documentation](https://docs.rs/cc)

A simple library meant to be used as a build dependency with Cargo packages in
order to build a set of C/C++ files into a static archive. This crate calls out
to the most relevant compiler for a platform, for example using `cl` on MSVC.

## Using cc-rs
    :

画面が暴走ぎみで、かろうじて、下記を採取出来た。

[src/mini_markdown.rs:11] tokens = [
    Header(
        1,
        "cc-rs",
        None,
    ),
    Newline,
    Plaintext(
        "A library to compile C/Cnegative string lengthnegative string .....

最初にヘッダーが徠て、次は改行。3行目で、普通のテキスト文字って認識したんだけど、 その中の++を特別扱いしちゃってループに陥っているとわかった。

憎い++を削除して走らせてみると、ちゃんと最後まで走った。mon()を少し改造して結果を見ると、

[src/mini_markdown.rs:11] tokens.last() = Some(
    Plaintext(
        "A library to compile C/C/assembly into a Rust library/application",
    ),
)
[src/mini_markdown.rs:11] tokens.last() = Some(
    Plaintext(
        "A library to compile C/C/assembly into a Rust library/application.",
    ),
)
[src/mini_markdown.rs:11] tokens.last() = Some(
    Newline,
)
[src/mini_markdown.rs:11] tokens.last() = Some(
    Link(
        "https://docs.rs/cc",
        Some(
            "Documentation",
        ),
        None,
    ),
)

Plaintextの解析は1文字づつ行われていた。lex内で1文字づつスキャンして、# とかの特殊文字(勿論 +,-,*等)は、それぞれのルーチンで処理される。それ以外は、何でもマッチするって亊で、それがPlanintextになるんだな。だから、おかしくなってたとな。

encodingrs-0.8.30/README.md の一節

buffer contains only Latin1 code points (below U+0100).

こんな風に引掛っていた。

[src/mini_markdown.rs:11] tokens.last() = Some(
    Plaintext(
        "  buffer contains only Latin1 code points (below Unegative string lengthnegative string lengthnegative ...

荒療治

オイラーはブラック・ジャックじゃない。だから悪い所は冷徹に取り除いてしまう外科医なのさ。切り刻むのが大好き。

悪い所は、取り払ってしまいましょう。有無を言わさずにね。迷っていてもしょうがない。 mini_markdown.rs

// '-' | '+' => {
//     let token = lex_plus_minus(&mut char_iter);
//     match token {
//         Ok(t) => tokens.push(t),
//         Err(e) => push_str(&mut tokens, e.content),
//     }
// },

切除したら、ちゃんと動くようになった。

標本作成、解析、修整

まあ、悪いREADME.mdはあれだな。じゃ進歩がないので、標本を作ってみる。

[sakae@fb /tmp/mdm]$ cat README.md
# test plus
below U+0100

これを喰わせると発症する。そう永久増殖しちゃう癌です。止らなくなります。

pub(crate) fn lex_plus_minus(
    char_iter: &mut std::iter::Peekable<std::str::Chars>,
) -> Result<Token, ParseError> {
    let s = consume_while_case_holds(char_iter, &|c| c == &'-');
    match s.len() {
        3..=usize::MAX => return Ok(Token::HorizontalRule),
        2 => return Ok(Token::Plaintext(s)),
        1 => {}
        _ => {
            return Err(ParseError {
                content: "negative string length".to_string(),
            })
        }
    }
    let line = consume_while_case_holds(char_iter, &|c| c != &'\n');
     :

主目的は、タスクリストの処理です。が、最初の長さチェックで、引掛ってしまい、エラーが返却される。そのエラー文字を呼出側でREADME.mdの文字列に混入させてしまうものだから、いつまでたっても、ここを抜け出せないっていう永久ループの完成です。

馬の前に人参をぶら下げて、走らせる様です。いつまでたっても人参にありつけないものだから、永久に走り続けてしまうという哀れ。

match s.len() {
    3..=usize::MAX => return Ok(Token::HorizontalRule),
    2 => return Ok(Token::Plaintext(s)),
    _ => {}
}

こんな、やる気の無い修整したよ。いや、これだって、りっぱな温存手術だろう。でもこれじゃ、癌撲滅でノーベル賞は永久に貰えないな。やっぱり、やぶ病理医者だ。

mini_markdown/README.md の特殊事例対処

これは、珍しい事例だ。作者さんがわざと仕込んでおいたと思われるぞ。

w3mでアクセスすると、こんな風になる。

README.md

 mini_markdown

A dependency free markdown renderer.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

The design goal of this project is to provide a dependency free, feature
  :

これと生のREADME.mdを見比べると、___ っていうのがブラウザーでは、<hr> だっだかに変換されて表示されるはずが、ただのアンダーラインと認識。ようするに、そんな機能は実装されていない。それで、次の知ってるタグが出て来て、切り替わるまで、だらだらと表示って亊だ。

ガン無視でいいな。

これで最後か?

大体使える所まで漕ぎ着けたと思えるので、w3mを呼び出すようにしよう。 パイプの例が、サンプルで掲載されてたので、真似をしてみる。

sent pangram to w3m
w3m responded with:
test plus

below U+0100

エラーもなく実行出来たんだけど、w3mの画面は開かずに、こんな表示になった。何で何で?

風呂に入っている時に、はたと気が付いた。まるで、アルキメデスみたいです。裸踊りはしなかったけど、翌朝が待ちどうしい。そうさ、w3mはフィルターとして働き、レンダリング結果をパイプを使って返してきただけじゃん。

ならば、パイプのうち一本を削除してあげればいいだろう。朝の実験結果。

sent pangram to w3m
ob$ Error occurred while reset 588: errno=5

パイプは双方向に張られていないとだめって亊なんだろうね。

第二版

パイプ接続は諦めて、htmlファイルをw3mに渡すと言う方法で、妥協する。

mod mini_markdown;
use crate::mini_markdown::lex;
use crate::mini_markdown::render;
use std::io::Write;
use std::process::Command;

fn main() {
    let mds = std::fs::read_to_string("README.md").expect("Need README.md");
    let bdy = render(&mds);
    let html = format!(
        "<html><head><title>README.md</title></head><body>{}</body></head>",
        bdy );

    let dmy = "/tmp/zREADMEmd.html";
    let mut file = std::fs::File::create(&dmy).unwrap();
    writeln!(file, "{}", html).unwrap();

    let mut proc = Command::new("w3m").arg(&dmy).spawn().unwrap();
    let _result = proc.wait().unwrap();

    std::fs::remove_file(&dmy).expect("Can't remove");
}

dmyってのでファイル名を決めうちしてる。本当ならその場でランダムなファイル名を作成するのが筋だろうけど、まあ個人利用って亊で許してね。

ob$ cargo b --release
ob$ cp target/release/mdm /home/sakae/.cargo/bin/

OpenBSD付属のコンパイラーで作ると、サイズが615Kになった。Debian 64Bitでは、3.8Mもあった。この差は何よ? 年式の違いなの?

FreeBSDには新しい版が入っているので、確認。

[sakae@fb /tmp/mdm]$ ls -lh target/release/mdm
-rwxr-xr-x  2 sakae  wheel   649K Mar  1 06:30 target/release/mdm*
[sakae@fb /tmp/mdm]$
[sakae@fb /tmp/mdm]$ ldd target/release/mdm
target/release/mdm:
        libthr.so.3 => /lib/libthr.so.3 (0x206e1000)
        libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x2070a000)
        libc.so.7 => /lib/libc.so.7 (0x2045e000)
[sakae@fb /tmp/mdm]$ rustc -V
rustc 1.58.1

使いかたは、README.mdが有る所で、mdm って叩くだけです。

~/.cargo/registry/src/github.com-1ecc6299db9ec823 こんな所に、全記録が残っているので、適当なdirの中に入って確かめてみて。

色々な木箱を使ってる時、cargo vendor すると、関係する木箱がvendorってdirに集ってくるので、そこで見るのの好いと思う。

失敗の本質

今回の永久ループになる原因は、エラーを検出した時に、原因理由の文字列を解析の文字ストリームに戻している亊にあった。しかも戻している文字が、本流とは全く異った文字列。

今、できそこないな人間なんて本を見てる。細胞分裂でDNAが次世代に伝わって行くんだけど、コピーミスが重なり、ジャンクDNAが多数あるらしい。それと同じっぽいな。

ソースを見ると、同様な手法があちこちで使われている。よって、まだ隠れたBUGが内在していてもおかしくない。

色々なREADME.mdを閲覧していると、解析に失敗してると思われるものが散見される。ざっと見する分には、実害が無いので笑って許してる。

前回見付けた、同業他者のソフトはどうやっているのだろう? 参考まで見てみるかな。

markdown.rs

sakae@pen:/tmp$ git clone https://github.com/johannhof/markdown.rs.git
sakae@pen:/tmp/markdown.rs$ cargo r -- README.md | w3m -T text/html
sakae@pen:/tmp/markdown.rs$ cargo vendor
sakae@pen:/tmp/markdown.rs$ ls vendor/
aho-corasick  getopts      memchr    regex         unicode-width
difference    lazy_static  pipeline  regex-syntax
sakae@pen:/tmp/markdown.rs$ cargo r -- vendor/regex/README.md | w3m -T text/html
sakae@pen:/tmp/markdown.rs$ cd vendor/regex
sakae@pen:/tmp/markdown.rs/vendor/regex$ mdm

少し研究してみるか。regexなんてのが見えるんで、これは正規表現法か。最勉強が必要だろうね。

異業界では、 Go: Terminal上でmarkdownをプレゼンできるslides こんなのも有るなあ。簡単な記法なんで、色々と応用出来そう。回を改めてみるか。


This year's Index

Home