oguna / rustmigemo

12 stars 1 forks source link

yet-another-migemo-dict を利用すると panic してしまう #1

Open tokuhirom opened 8 months ago

tokuhirom commented 8 months ago

https://github.com/oguna/yet-another-migemo-dict/releases/tag/v0.4 で配布されている migemo-compact-dict.zip を利用して

cargo build && ./target/debug/rustmigemo -d $HOME/Downloads/migemo-compact-dict

と実行したときに、's' や 'h' などの特定のクエリを実行したときに、panic してしまいます。

QUERY: s
thread 'main' panicked at src/migemo/regex_generator.rs:252:83:
called `Option::unwrap()` on a `None` value
stack backtrace:
   0: rust_begin_unwind
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14
   2: core::panicking::panic
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:144:5
   3: core::option::Option<T>::unwrap
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/option.rs:931:21
   4: rustmigemo::migemo::regex_generator::RegexGenerator::generate_stub
             at ./src/migemo/regex_generator.rs:252:14
   5: rustmigemo::migemo::regex_generator::RegexGenerator::generate
             at ./src/migemo/regex_generator.rs:186:5
   6: rustmigemo::migemo::query::query_a_word
             at ./src/migemo/query.rs:40:21
   7: rustmigemo::migemo::query::query
             at ./src/migemo/query.rs:51:23
   8: rustmigemo::main
             at ./src/main.rs:98:26
   9: core::ops::function::FnOnce::call_once
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
diff --git src/migemo/regex_generator.rs src/migemo/regex_generator.rs
index 9ec12b8..111fe60 100644
--- src/migemo/regex_generator.rs
+++ src/migemo/regex_generator.rs
@@ -249,6 +249,10 @@ impl RegexGenerator {
                                {
                                        buf.push('\\');
                                }
+                               println!("TMP====");
+                               println!("{:?}", tmp.as_ref().unwrap().code);
+                               println!("{:?}", tmp.as_ref().unwrap().code as u32);
+                               println!("{:?}", std::char::from_u32(tmp.as_ref().unwrap().code as u32));
                                buf.push(::std::char::from_u32(tmp.as_ref().unwrap().code as u32).unwrap());
                                if operator.newline.len() > 0 {
                                        buf.push_str(&operator.newline);

のように printf debug をしますと、以下のようになります。

(前略)
TMP====
40635
40635
Some('麻')
TMP====
40653
40653
Some('黍')
TMP====
55362
55362
None
thread 'main' panicked at src/migemo/regex_generator.rs:256:83:
called `Option::unwrap()` on a `None` value
stack backtrace:
   0: rust_begin_unwind
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14
   2: core::panicking::panic
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:144:5
   3: core::option::Option<T>::unwrap
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/option.rs:931:21
   4: rustmigemo::migemo::regex_generator::RegexGenerator::generate_stub
             at ./src/migemo/regex_generator.rs:256:14
   5: rustmigemo::migemo::regex_generator::RegexGenerator::generate
             at ./src/migemo/regex_generator.rs:186:5
   6: rustmigemo::migemo::query::query_a_word
             at ./src/migemo/query.rs:40:21
   7: rustmigemo::migemo::query::query
             at ./src/migemo/query.rs:51:23
   8: rustmigemo::main
             at ./src/main.rs:98:26
   9: core::ops::function::FnOnce::call_once
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5

よって、意図しない 55362 によってエラーになっているようです。 コードの詳細は追っていないのですが、 55362 は十六進数で 0xD842 なのでUnicodeのHigh Surrogates (U+D842) だと思います。サロゲートペアに現在の rustmigemo が対応していないにもかかわらず、yet-another-migemo-dict には surrogate pair で表現されるような文字列が含まれているために発生しているのかなと想像しています。

  1. サロゲートペアに対応するように rustmigemo を修正する
  2. サロゲートペアで表現されるような文字は重要度が低いと考えて辞書の生成時点で除外してしまう。

などの対応策が考えられるのかなと思いました。

(なお、https://github.com/oguna/migemo-compact-dict-latest の辞書では問題なく動作しております)

tokuhirom commented 8 months ago

サロゲートペアを含む文字を https://github.com/oguna/yet-another-migemo-dict/releases/tag/v0.4 の migemo-dict から以下のスクリプトを利用して検出してみました。

import codecs
import sys

def is_surrogate_pair(char):
    """
    Checks if a character is in the surrogate-pair range.
    """
    return 0x010000 <= ord(char) <= 0x10FFFF

infile = sys.argv[1]
with codecs.open(infile, 'r') as file:
    for line in file:
        has_surrogate_pair = False
        for char in line:
            if is_surrogate_pair(char):
                print("U+%X" % ord(char))
                line = line.replace(char, f'**{char}**')
                has_surrogate_pair = True
        if has_surrogate_pair:
            print(line)

出力は以下のようになります。

U+20B9F
しか    仕懸    仕掛    仕替    併      兪      叱      史家    呵      四角    始華    子夏    子華    市価    市花    師家    志嘉    志賀    歯科    死花    然
        知客    確      確然    私家    糸価    紙価    紙花    緊      而      聢      芝河    詆      詞花    詞華    詩家    詩歌    賜暇    蹙      顰      飾
        **𠮟**

U+20B9F
しっ    七      卓      叱      執      失      嫉      尻      悉      櫛      湿      漆      濕      疾      確      緊      質      **𠮟**

U+20B9F
しつ    七      仕付    仕詰    叱      喞      執      失      嫉      室      志都    悉      桎      湿      漆      濕      為付    為詰    瑟      疾      膝
        蟋      貭      質      躾      隰      隲      **𠮟**

U+27631
ふき    不帰    不羈    不覊    不記    不諱    不軌    付記    吹      富喜    富貴    布岐    腐気    苳      葺      蕗      袘      附記    **𧘱**

該当する文字は、以下の2つであり、正直なところ、これらの文字を辞書に含めないということにしても、あまり困らないかな、という気もします。

https://www.compart.com/en/unicode/U+20B9F https://www.compart.com/en/unicode/U+27631

tokuhirom commented 8 months ago

https://github.com/oguna/yet-another-migemo-dict/pull/3 こちらで、surrogate pair を含む文字を compact 辞書から除外する処理を実装してみました。

oguna commented 6 months ago

返事が遅れて申し訳ございません。 バグ報告ありがとうございます。

サロゲートペアについては深く考えずに作っていたため、サロゲートペアを含む文字でクラッシュを起こしてしまいました。 サロゲートペアを含む文字が辞書に含まれているとは思ってなかったのですが、誤って含まれていました。

さて、いろいろ調査をしていたのですが、プログラミング言語により、正規表現でのサロゲートペアの扱いが異なるようです。

例えば、JavaScriptでは、サロゲートペアの文字を2文字として受け入れます(uフラグがない場合)。 上位のサロゲートペアが共通の場合、まとめて、下位のサロゲートペアを[...]で囲めば、連続していないサロゲートペアであっても、正規表現にマッチします。

const re1 = new RegExp('\u{D842}[\u{DF9F}\u{DFB7}a]')
console.log('𠮟a', re1.exec('𠮟a'))
console.log('𠮷', re1.exec('𠮷'))

出力は以下です。

𠮟a [ '𠮟', index: 0, input: '𠮟a', groups: undefined ]
𠮷 [ '𠮷', index: 0, input: '𠮷', groups: undefined ]

Rustだと、以下のコードはエラーとなります。D842というコードポイントの文字はないからです。

let re1 = Regex::new(r"\u{D842}[\u{DF9F}\u{DFB7}a]").unwrap();
println!("{:?}", re1.find("𠮟a"));
println!("{:?}", re1.find("𠮷"));

このように、プログラミング言語により正規表現の解釈が変わりますし、加えて、このツール、特にrustmigemoは、CLIで他のツール(例えばVim)に入力することを想定しているため、入力するツールでの影響も調査しなければなりません。 そのため、省メモリ版Migemoはサロゲートペア対応をうたってはいませんでした。

で、どうしたものかといろいろ考えていたのですが、 結論としてはサロゲートペアが含まれた辞書ファイルに対応しました。 正規表現を生成するregex_generator.rsの内部で使う文字コードをUTF32とすることで解決しました。 問題なく動作するかお手数ですがご確認ください。

修正コミット: fe2079787bd7475a471404e996a217a1a36ca4c7