以降の話は日本語のみならず、多国語/マルチバイト文字一般に言えることで あると思われるが、話がややこしくなるので、特に断りのない場合は日本語、 しかも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での挙動は確認していない。
実は同一視されるのは「は」と「わ」だけではなく、同一視される組み合わせは 数え切れないほど存在する。このことがあまり知られていないようなのは、
同一視される文字の組を以下にいくつか示す。 全てを挙げるのは大変であるし、時間の無駄であろう。
は と わ | ハ と ワ | A と a | Z と 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() を以下に引用する:
見てわかるとおり、マルチバイト文字列について一切考慮されていない。
smb_hash_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;
}
ここで使われている tolower() が曲者で、アルファベット大文字小文字各26字だけを
変換してくれるものであったなら(eucはその範囲を使わないので、少なくともeucでは)問題
なかったのであるが、そうなっていなかった。
ここで tolower() では、/usr/src/linux-x.x.x/lib/ctype.c で定義
されている判定テーブルが使われるのだが、これが:
などとなっている。ソース中の _U が「大文字です」、_L が「小文字です」を意味する。
見づらいが、それでも128番以降の文字にもごっそり _U _L が使われていることがわかる(笑)。
非英語圏(かつ非漢字圏)を意識してのことであろう。
そしてこの _U がついているコードが、上で述べた 0x41 〜 0x5a , 0xc0 〜 0xd6 , 0xd8 〜 0xde
の範囲である。
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 */
いくつかの方法が考えられる。
最も根本的解決になると思われるのは、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ではないため、はわ〜問題(仮称)はない。