一部の日本語フォルダ名でインデックスが作成されない件、とその回避パッチ

結論

Hyper Estraier バージョン1.4.13を日本語Windows環境で使用した場合:

  • estcmd gatherの検索対象に、0x5cで終わるフォルダ名があると、そのフォルダ以下はインデックスが作成されない。このため、検索漏れが生じる。
  • 原因は、estcmdのディレクトリスキャンルーチンのコードと、MinGWのopendir()の実装がマルチバイト用の文字処理ルーチンを呼んでいないことの2つ。
  • estcmd.cに対するワークアラウンドパッチ

勤め先の部門サーバに、HyperEstraierによる全文検索システムを導入しました。すると、使った人から「入れたはずの価格表が検索にかからない」との指摘。ログを確認してみると、”xxx価格表”サブディレクトリ以下がごっそりスキャンすらされていません。なんでだーと思い、じっと眺めていると、...\xxx価格表\hoge.txt...
表という文字がなんかいかにも怪しそうです。

現象確認

試しに単純なディレクトリ構造を作り、スキャンだけやってみると:

C:\tmp>tree /F c:\tmp\testest
フォルダ パスの一覧
ボリューム シリアル番号は xxxx-xxxx です
C:\TMP\TESTEST
├─テスト
│      価格表.txt
│
└─テスト表
        その他価格表.txt

C:\tmp>estcmd scandir c:\tmp\testest
c:\tmp\testest
c:\tmp\testest\テスト
c:\tmp\testest\テスト\価格表.txt
c:\tmp\testest\テスト表
c:\tmp\testest\テスト表テスト表

やはり、”テスト表”サブディレクトリ以下は正しく読まれていません。おそらくディレクトリスキャンルーチンの文字列処理だけだろうとあたりをつけて、調べてみることにしました(先日の再ビルドする必要→d:id:aenomoto:20080210、はこのためでした)。

修正点1

まず、scandirサブコマンドを実行しているディレクトリスキャンルーチンに以下の箇所があります。

estcmd.c, procscandir():

  path = (len > 0 && line[len-1] == ESTPATHCHR) ? cbsprintf("%s%s", line, tmp) :
    cbsprintf("%s%c%s", line, ESTPATHCHR, tmp);

ディレクトリ名とファイル名を繋げて、パス名を組み立てているのですが、間にディレクトリ区切り文字('\')をはさむかどうかの判定に、ディレクトリ名文字列の最終バイトを直接'\'と比較しています。これだと、ディレクトリ名が日本語で最終文字の2バイト目が0x5cの場合と、本当に半角の'\'で終了している場合と、区別することができません。
(2バイト目が0x5cの文字で有名なやつの一つが’表’です。参考→http://katsura-kotonoha.sakura.ne.jp/prog/etc/tip00001.shtml

以下のように書き換えます。

  unsigned char* _mbsrchr(const unsigned char* string, unsigned int c);

  if (len > 0 && (_mbsrchr(line, ESTPATHCHR) == line + len - 1)) {
    path = cbsprintf("%s%s", line, tmp);
  }
  else {
    path = cbsprintf("%s%c%s", line, ESTPATHCHR, tmp);
  }

ディレクトリ名の末尾がパス名区切り文字かは、strrchr()で最期に出現する'\'文字の位置を探索し、それが文字列の最終バイトかどうかで判断します。_mbsrchr()はVC++ランタイムの拡張関数で、strrchr()のマルチバイト版です。MinGWのバイナリは、最終的にはVC++ランタイムにリンクされるので、きちんとプロトタイプ宣言してあげればそのまま使えるはずです。

上記の処理はestcmd.c中に2ヶ所あります(scandir/gather)。それぞれを上記のコードに差し替えます。

修正点2

以上の修正で"テスト表"サブディレクトリ後の'\'が落ちる現象はなくなりますが、まだその下のファイルを正しく検出できません。

これは、MinGWUNIX互換ディレクトリスキャンルーチンの実装に問題があるようです。opendir()/readdir()を呼び出すだけの単純なプログラムでも(こんなの→test-opendir.c):

>test-opendir.exe C:\tmp\testest\テスト
d_name=>'.'
d_name=>'..'
d_name=>'価格表.txt'

>test-opendir.exe C:\tmp\testest\テスト表
d_name=>'テスト表'

となってしまいます。
MinGWでopendir()/readdir()を実装しているランタイムライブラリのソースを確認してみます。mingw-runtime-3.14-src.tar.gzの内容によれば、

mingwex/dirent.c, _topendir():
...
  /* Add on a slash if the path does not end with one. */
  if (nd->dd_name[0] != _T('\0')
      && _tcsrchr (nd->dd_name, _T('/')) != nd->dd_name
					    + _tcslen (nd->dd_name) - 1
      && _tcsrchr (nd->dd_name, _T('\\')) != nd->dd_name
      					     + _tcslen (nd->dd_name) - 1)
    {
      _tcscat (nd->dd_name, SLASH);
    }
...

となっていて、修正点1と同じ手法で引数文字列がパス名区切り文字で終わっているか判定し、必要なら付加しています(というか、このコードを見て修正点1のコードを書いたのですが:-)。
ここで_tcsrchr()はWindowsの汎用文字型TCHAR用のstrrchr()ですが、実際の置き換えは(当然)Microsoftのヘッダではなく、独自のtchar.hで行われています。

include/tchar.h:

#ifdef _UNICODE
#define	_tcsrchr	wcsrchr
#else
#define	_tcsrchr	strrchr
#endif

これ、確かにMicrosoftのコードと同じ挙動ですが、MicrosoftのTCHARルーチンは、さらに_MBCSマクロの影響も受けます。概念的には、

#ifdef _UNICODE
# define	_tcsrchr	wcsrchr
#else
# ifdef _MBCS
#  define	_tcsrchr	_mbsrchr
# else
#  define	_tcsrchr	strrchr
# endif
#endif

結局、MinGWランタイムのopendir()はパス名区切り文字の検索にマルチバイトには非対応のstrrchr()を使用し、修正1と同じ理由で"テスト表"フォルダのパス名区切り付加に失敗します。MinGWのopendir()は、この後さらに'*'を付加してAPIによるディレクトリ探索を実行しているので、正しいファイルリストは得られないことになります。

これの修正は、MinGWランタイムを日本語Windowsではマルチバイト版の文字列処理関数を呼ぶようにできればいいのでしょうが、それはちょっと面倒そうです。

上記の処理は、opendir()の引数として渡された文字列が'\'で終端されていない場合の処理です。ということは、opendir()呼び出し時に、引数の末尾に必ず'\'が付くようにしてあげればこの問題を回避できます。

HyperEstraier側で実際にopendir()を呼び出しているのは、本体ではなくQDBMです。QDBMにディレクトリリストを作成するサービスルーチンがあり、

cabin.c, cbdirlist():

CBLIST *cbdirlist(const char *name){
...
  assert(name);
  if(!(DD = opendir(name))) return NULL;
  CB_LISTOPEN(list);
...

となっています。引数のnameは直接opendir()に渡されているので、HyperEstraier本体のcbdirlist()呼び出し時に、ディレクトリ名末尾に'\'を付加するようにしてしまいましょう。

CBLIST *mbwa_cbdirlist(const char *name)
{
...
  //ディレクトリ名に細工する
...
  return cbdirlist(name);
}

...こんな感じに。estcmd.c内でcbdirlist()の呼び出しをこの関数に置き換えれば、ワークアラウンドパッチの対象を小さく、estcmd.cファイルひとつだけにできます。

というわけで、以上2つの修正を組み合わせたパッチ→hyperestraier-1.4.13-mbfs-workaround.patch

再テスト

C:\tmp>estcmd scandir c:\tmp\testest
c:\tmp\testest
c:\tmp\testest\テスト
c:\tmp\testest\テスト\価格表.txt
c:\tmp\testest\テスト表
c:\tmp\testest\テスト表\その他価格表.txt

これで、サーバにある、なんとか価格表だの、なんたら予定表だのといったフォルダも検索できるようになりました。