smbfsにおける漢字ファイル名の「はわ〜問題」(仮称)

 以降の話は日本語のみならず、多国語/マルチバイト文字一般に言えることで あると思われるが、話がややこしくなるので、特に断りのない場合は日本語、 しかもLinux側はeuc、Windows側はsjis(cp932)の文字セットを使っている場合に限定する。


はわ〜問題(仮称)とは

 LinuxではWindowsのSMB(CIFS)共有をマウントすることができる。 マウント時のオプションにより、ファイル名の文字コード変換も自動的に行えるので、 日本語のファイル名も、ローカルのファイルシステムと同じように扱うことができる。

 ところが日本語の扱いに不備があり、いろいろな要因も重なって、結果的に例えば 「は」という文字と「わ」という文字が同一視されてしまう。これが「はわ〜問題(仮称)」 である。遭遇した人間が思わず「はわわわ〜〜〜!?」と慌ててしまうところから 名づけられたかどうかは不明である。・・・・・・

 ・・・試しにやってみよう。

# mount -t smbfs -o 'codepage=cp932,iocharset=euc-jp' //windows/smb /mnt/smb
Password:
# cd /mnt/smb
 これから日本語のファイル名を扱うので、上のようにオプションをつけてマウントした。
# ls -l
合計 0
# touch は
# mkdir わ
mkdir: `わ' は存在しますがディレクトリではありません
 このように、「は」ファイルを作ったあと「わ」フォルダを作ろうとすると、 はじかれてしまう。「わ」は既に存在していることになっているらしい(笑)
# ls -l
合計 0
-rwxr-xr-x    1 root     root            0 11月 15 13:31 は
# rm わ
# ls -l
合計 0
 また、作ったファイルは「は」なのに、しらんぷりして「わ」を削除しようとすると、 「は」が削除されてしまう。
# echo "test test test" > わ
# cat は
test test test
 今度は「わ」を作って、しらんぷりして「は」を表示してみた。 「わ」の内容が表示できてしまう(笑)

 次に、Windows側で「は」と「わ」の2つのファイルを作ってみた。 判りやすいように、それぞれのファイルには「はははー!」と「わわわー!」という 文字列を(eucで)書き込んである。

# ls -l
合計 1
-rwxr-xr-x    1 root     root           12 11月 15 13:55 は
-rwxr-xr-x    1 root     root           12 11月 15 13:55 わ
# cat は
わわわー!
# cat わ
わわわー!
「は」と「わ」の両方のファイルがあっても、「わ」の方にしかアクセス できないことがわかる。


この問題による影響の範囲

 この問題が発生するのはsmbmountを使った場合 (mountでsmbfsファイルシステムを指定した場合) だけであり、smbdやsmbclientでは (この問題は)発生しない。また、smbshでの挙動は確認していない。

 実は同一視されるのは「は」と「わ」だけではなく、同一視される組み合わせは 数え切れないほど存在する。このことがあまり知られていないようなのは、

  1. 同一視される文字の組み合わせパターンは無数にあり、偶然一致することは少ない
  2. smbmountは、smbd/nmbd等に比べると、使われる機会がそもそも少ない
  3. smbmountを使うような人で、かつファイル名に日本語を使う人はさらに少ない(笑)
 為ではないかと思われる。

 同一視される文字の組を以下にいくつか示す。 全てを挙げるのは大変であるし、時間の無駄であろう。
は と わハ と ワA と aZ と z〜 と =
だ と むち と めつ と やて と ゆと と よ
な と りに と るぬ と れね と ろひ と を
粟 と 萎雲 と 園伽 と 迦澄 と 燹線 と 珱

 上記のようにいくつかの文字が同一視されることにより、

 などといったファイル名(こんなファイル名をつけることがあればだが)は、 同じディレクトリに共存することができない。 もちろん、どちらか一方づつならば普通に存在できる。


文字が同一視されてしまう条件

 実装に依存するため、「私の環境での場合は」という断り書きが常につくが、 この問題が起きる条件は次のとおりである。

・マルチバイト文字列中に、0x41 〜 0x5a , 0xc0 〜 0xd6 , 0xd8 〜 0xde の範囲のうち いずれかの値をもつバイトが現れる場合。

 該当する値はそれぞれ 0x61 〜 0x7a , 0xe0 〜 0xf6 , 0xf8 〜 0xfe の値に 変換(tolower)されたうえで比較されるので、変換後のコードで表される文字と 同一視されてしまう。


詳細

 この問題はsambaのバージョンによらず発生するようである。 Samba 2.2.7a , Samba 2.2.8a-ja-1.1 , Samba 3.0 で全て同じ現象がみられた。

 smbfsをマウントしたときだけに発生する問題であるから、 カーネル側の問題ではないかと思われる。 いちおう付記しておくと、私の環境のLinuxのバージョンは 2.4.20-20.9 (RedHat Linux 9) である。

 smbfs関係のソースは、/usr/src/linux-x.x.x/fs/smbfs/ 以下に格納されている。 問題の関数は dir.c にある smb_compare_dentry() および smb_hash_dentry() である。 Linuxでは通常、ファイル名の大文字小文字は区別されるが、smbfsでは同一視されるので、 これらの関数は大文字小文字を同一視した比較 およびハッシュ値算出を行うようになっている。 例えば、smb_compare_dentry() を以下に引用する:

static int
smb_compare_dentry(struct dentry *dir, struct qstr *a, struct qstr *b)
{
    int i, result = 1;

    if (a->len != b->len)
        goto out;
    for (i=0; i < a->len; i++) {
        if (tolower(a->name[i]) != tolower(b->name[i]))
            goto out;
    }
    result = 0;
out:
    return result;
}
 見てわかるとおり、マルチバイト文字列について一切考慮されていない。 smb_hash_dentry() も似たようなものである。これが「はわ〜問題」(仮称)の直接の原因である。

 ここで使われている tolower() が曲者で、アルファベット大文字小文字各26字だけを 変換してくれるものであったなら(eucはその範囲を使わないので、少なくともeucでは)問題 なかったのであるが、そうなっていなかった。 ここで tolower() では、/usr/src/linux-x.x.x/lib/ctype.c で定義 されている判定テーブルが使われるのだが、これが:

unsigned char _ctype[] = {
_C,_C,_C,_C,_C,_C,_C,_C,			/* 0-7 */
_C,_C|_S,_C|_S,_C|_S,_C|_S,_C|_S,_C,_C,	/* 8-15 */
_C,_C,_C,_C,_C,_C,_C,_C,			/* 16-23 */
_C,_C,_C,_C,_C,_C,_C,_C,			/* 24-31 */
_S|_SP,_P,_P,_P,_P,_P,_P,_P,		/* 32-39 */
_P,_P,_P,_P,_P,_P,_P,_P,			/* 40-47 */
_D,_D,_D,_D,_D,_D,_D,_D,			/* 48-55 */
_D,_D,_P,_P,_P,_P,_P,_P,			/* 56-63 */
_P,_U|_X,_U|_X,_U|_X,_U|_X,_U|_X,_U|_X,_U,	/* 64-71 */
_U,_U,_U,_U,_U,_U,_U,_U,			/* 72-79 */
_U,_U,_U,_U,_U,_U,_U,_U,			/* 80-87 */
_U,_U,_U,_P,_P,_P,_P,_P,			/* 88-95 */
_P,_L|_X,_L|_X,_L|_X,_L|_X,_L|_X,_L|_X,_L,	/* 96-103 */
_L,_L,_L,_L,_L,_L,_L,_L,			/* 104-111 */
_L,_L,_L,_L,_L,_L,_L,_L,			/* 112-119 */
_L,_L,_L,_P,_P,_P,_P,_C,			/* 120-127 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,		/* 128-143 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,		/* 144-159 */
_S|_SP,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,   /* 160-175 */
_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,_P,       /* 176-191 */
_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,_U,       /* 192-207 */
_U,_U,_U,_U,_U,_U,_U,_P,_U,_U,_U,_U,_U,_U,_U,_L,       /* 208-223 */
_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,_L,       /* 224-239 */
_L,_L,_L,_L,_L,_L,_L,_P,_L,_L,_L,_L,_L,_L,_L,_L};      /* 240-255 */
 などとなっている。ソース中の _U が「大文字です」、_L が「小文字です」を意味する。 見づらいが、それでも128番以降の文字にもごっそり _U _L が使われていることがわかる(笑)。 非英語圏(かつ非漢字圏)を意識してのことであろう。 そしてこの _U がついているコードが、上で述べた 0x41 〜 0x5a , 0xc0 〜 0xd6 , 0xd8 〜 0xde の範囲である。


対処法

 いくつかの方法が考えられる。

 最も根本的解決になると思われるのは、smb_compare_dentry() および smb_hash_dentry() を マルチバイト文字列対応にすることである。とりあえず現在の実装は、マルチバイト対応を 考えずに5分で書いたような印象がぬぐえない。(^_^;

 しかし、単にマルチバイト文字列対応といっても、ことはそう単純ではない。 Windowsのファイル名の比較方法と同じにしなければならない。 これは実はかなり困難である。例えばWindowsは全角の「i」と「I」も同一視する。 Windowsの文字セットはLinuxのそれと微妙に違う(らしい)。繋ぐ先によって比較方法を変えなければ ならないだろう。そのせいか、同じく大文字小文字を同一視するfat/vfatの該当部分はかなり ややこしいことになっている。カーネルではなく、samba側に smb_compare_dentry() および smb_hash_dentry() に相当する処理をやらせた方がすっきりするかもしれない。 いずれにしてもコーディング量は多めになるだろうが、この方法をとればよい結果が得られるだろう。

 次善の策としては、smb_compare_dentry() と smb_hash_dentry() で使われている tolower() を自前で用意して、とりあえずeucの場合に不具合が出ないようにすること が考えられる。 あるいは、eucに特化したマルチバイト文字列対応を行うか。 eucに限定すれば、必要なコーディング量はぐっと少なくなるだろう。 しかし、euc以外の文字セットを使う場合は別の不具合が出るかもしれない。 また、依然として「i」と「I」を同一視すべきところを、区別されてしまうという 問題は残る。

 もうひとつ、最も単純な方法であるが、いっそきっぱり、大文字小文字を同一視する という動作をやめてしまう方法である。実はそういうオプションが既にあり、 カーネルを再構築する必要がない。具体的にはこうする。

# mount -t smbfs -o 'case,codepage=cp932,iocharset=euc-jp' //windows/smb /mnt/smb
Password:
# cd /mnt/smb
 case というオプションを追加するだけである。
# touch は
# touch わ
# ls -l
合計 0
-rwxr-xr-x    1 root     root            0 11月 15 18:07 は
-rwxr-xr-x    1 root     root            0 11月 15 18:07 わ
 このように、「は」と「わ」が区別されている。

 しかし今度は別の問題があらわれる。 ファイル名の大文字小文字が区別されなくなってしまう。 同じく、実験してみよう。

# touch aaa
# ls -l
合計 0
-rwxr-xr-x    1 root     root            0 11月 15 18:17 aaa
# rm AAA
rm: remove 通常の空ファイル `AAA'? y
# ls -l
合計 0
 あれ?削除できる・・・(^_^;
# touch aaa
# mkdir AAA
mkdir: `AAA' は存在しますがディレクトリではありません
 あれ?存在することになってる・・・(^_^;
# echo "test test test" > aaa
# cat AAA
test test test
 あれ?普通に表示されてる・・・(^_^;

 どうも、全く不具合なく使えているような感じだ。(^_^;

 その後も少し実験してみたが、例えば aaa という名前のファイルを作った直後に、 AAA というファイルが存在するかチェックしようとすると、 「存在しない」という結果が返されることがあるようだ。 読み書きの入り乱れるシステムでは不具合が出るかもしれない。


付録

 はわ〜問題(仮称)があるかないかを簡単に調べられる スクリプトを作成したので紹介する。 引数を与えずに実行すると、カレントディレクトリにおいて、 はわ〜問題(仮称)があるかないかを表示する。 引数をふたつ与えると、ふたつの引数が同一視されるかどうかを調べて表示する。 このようにして使う。

# ./hawa-test
ここでは は と わ が同一視されています
# ./hawa-test 線 珱
ここでは 線 と 珱 が同一視されています
# cd ..
# smb/hawa-test
ここには は と わ が同一視される問題はありません
 1階層上のディレクトリは、smbfsではないため、はわ〜問題(仮称)はない。
トップ もどる