rb_enc_prev_char の動き

memo.sugyan.com

このブログを見て、rb_enc_prev_char の動きが気になった。

まず、 String#rindex がほんとに HAVE_MEMRCHR がないからかどうかを検証。

linuxruby をコードからビルド。

$ git clone https://github.com/ruby/ruby
$ cd ruby
$ autoconf
$ ./configure
$ make
$ ruby -e 'p "\x00\x01\x80\x00".rindex("\x01")'

linux では 1 になる。しかし、string.cstr_rindex() の内容を無理やり HAVE_MEMRCHR の false 側にして再実行すると 2 になる。ということは、false側の str_rindex() に問題があることがわかる。

rb_enc_prev_char の実装はここにある。 ruby/encoding.h at v3_1_3 · ruby/ruby · GitHub

rb_enc_prev_char の中では onigenc_get_prev_char_head を呼んでいるだけっぽいので onigenc_get_prev_char_head の実装を見てみる。実装はこちら ruby/regenc.c at v3_1_3 · ruby/ruby · GitHub

onigenc_get_prev_char_head の中では ONIGENC_LEFT_ADJUST_CHAR_HEAD を呼んでるだけっぽいので ONIGENC_LEFT_ADJUST_CHAR_HEAD の実装を見てみる。実装はこちら ruby/onigmo.h at v3_1_3 · ruby/ruby · GitHub

ONIGENC_LEFT_ADJUST_CHAR_HEAD の中では各エンコーディングの left_adjust_char_head が呼ばれているだけっぽい ruby のデフォルトエンコーディングは UTF8なので、実装はUTF8のエンコーディングのソースにある。 ruby/utf_8.c at v3_1_3 · ruby/ruby · GitHub

left_adjust_char_head では utf8_islead()じゃなかったらポインタのマイナスを繰り返す。

utf8_islead() の実装はこちら ruby/utf_8.c at v3_1_3 · ruby/ruby · GitHub

#define utf8_islead(c)     ((UChar )((c) & 0xc0) != 0x80)

文字を 0xc0 でマスクして 0x80 じゃなかったら true

ということは、0x80 ~ 0xBF なら ポインタがマイナスする。

str_rindex は pos を返すが、pos自体は1つずつしか減らないのに対して、rb_enc_prev_char は エンコーディングによって 2バイト以上減る可能性があることから問題が発生している。

ちなみに、Ruby3.2 ではこの点が解決されていて、 pos を返すのではなくて、 検索している文字列のポインタの位置から文字列の最初のポインタの位置を引いた値を返すようになっているので、ちゃんと 1 が返るようになっている。

ちなみに、 ruby 3.2 であっても、以下のようなコードは動作しない。

ruby -e 'p "\x00\x01\x80\x00".rindex("\x80")'

これは、検索文字列の \x80 が rb_env_prev_char でスキップされてしまうので検索文字列としてヒットしないため。

こういう場合はちゃんとエンコーディングを合わせるようにしないといけない。

ruby -e 'p "\x00\x01\x80\x00".b.rindex("\x80".b)'

まとめ

Ruby の文字列をバイト文字列として扱うには、そのままではダメ。ちゃんと String#b を使おう。