mount_msdosfs の文字コード¶
21世紀になってから20年近くも経つと言うのに、私たちは未だに 文字化けに取り憑かれている。滑稽である。
MS-DOS時代(だいたい1990年代後半)のMOディスクの中身を取り出そうと思い、 FreeBSD機に繋げて mount_msdosfs を使用したところ、ファイル名に関連して エラーが出た。カーネルハックして原因が判ったので、対策を記録しておく。
OS のバージョンは FreeBSD 11.2R 。
直面したエラーには 2種類あった。 チルダ記号(0x7e)に関するものと、「最」の字に関するもの。
チルダ記号(0x7e)のエラー¶
mount_msdosfs(8) は -D と -L オプションで文字コードを指定するようになっている。 -D はマウントするファイルシステムの文字コードの指定で、 今回は日本語MS-DOSで使用していたMOディスクなので SJIS になる。 -L の方はOS側の文字コードで、-L ja_JP.UTF-8 のようにロケール名を与える。 -D と -L を指定することで、カーネル内部で自動的に文字コードの変換が行われ、 ファイルシステムとOSの文字コードの違いが吸収される仕組みになっている。
さて、ネットを漁ると、mount_msdosfs には -L ja_JP.eucJP を与える方が良い、 と書かれたブログ記事が多いようだ。10年くらい前の記事で、かつて FreeBSD カーネルのマルチバイト化が不十分だった頃の対処法らしい。 しかし現在では UTF-8 でほぼ問題が出ないようだし、これから述べるエラーが出るので EUC-JP を指定する利点はあまりないように思う。
さてそのエラーだが、ファイル名にチルダ記号(0x7e)が含まれている時に発生する。 20年ほど前、MS-DOSとWindows95との併用期に、ファイル名の長さの違いを回避するために MS-DOS側のファイル名にチルダが付与され「REPORT~1.TXT」のようになったりしたことを 覚えている人も多いだろう。今回はこのチルダが悪さをする。
まず、条件と現象を記すと、 mount_msdosfs -D sjis -L ja_JP.eucJP でマウントし、 たとえば REPORT~1.TXT が含まれるディレクトリに対して ls(1) を実行すると、
REPORT?1.TXT: No such file or directory
となる。
原因は、実は SJIS と EUC-JP では 0x7e に割り当てられている文字が違うことにある。 EUC-JP は 0x7f 以下の 7ビット部分の文字セットは ISO646-US で、これは 0x7e が ASCII と同じくチルダ記号になっている。 一方 SJIS は 7ビット文字セットが ISO646-JP で、こちらの 0x7e はチルダではなく 上付き横棒 ‾ (UTF8:0xe280be, UCS2:0x203E)なのである。 さらに、この上付き横棒は EUC-JP には存在しないため、EUC-JP に変換しようとすると EUC-JP の INVALID 記号である〓(いわゆるゲタ文字、EUC-JP:0xAEA2)に置換される。
このため、カーネル内部で次のような処理が行われる。 仮にファイル名を REPORT~1.TXT とすると、 まず SJIS から UCS2 を経由して EUC-JP に変換する。
SJIS: 52 45 50 4f 52 54 7e 31 2e 54 58 54
UCS2: 52 45 50 4f 52 54 203e 31 2e 54 58 54
EUC-JP: 52 45 50 4f 52 54 aea2 31 2e 54 58 54
カーネルはゲタ文字化した文字列をファイル名として保持する。 カーネルからファイルシステムにアクセスする時には逆変換が行われ、 ゲタ文字は INVALID記号ではなく普通の文字として変換される。
EUC-JP: 52 45 50 4f 52 54 aea2 31 2e 54 58 54
UCS2: 52 45 50 4f 52 54 3013 31 2e 54 58 54
SJIS: 52 45 50 4f 52 54 ac81 31 2e 54 58 54
MS-DOSファイルシステムのファイル名は 8.3式なのに 変換後のものは 9.3になっている。9文字目が切り捨てられて、 またエンディアンの関係か下位バイトが先になるようなので、結局
52 45 50 4f 52 54 81 ac 2e 54 58 54
というバイト列を探すことになるが、もちろんこんな名前のファイルは ディレクトリエントリに存在しないので、ENOENT になる。
原因が判ったところで対策だが、SJIS の 0x7e がそのまま 0x7e になるように改造する 方法を試してみた。FreeBSD のソースコードに手を入れて色々試したところ、
/usr/src/share/i18n/esdb/MISC/Shift_JIS.src
/usr/src/share/i18n/esdb/MISC/Shift_JIS-2004.src
/usr/src/lib/libkiconv/quirks.c
を修正すれば良いらしい。
上の2つはSJISの文字セットを定義しているところで、 ファイル内の ISO646-JP のところを ISO646-US に変更する。 quirks.c は文字変換テーブルから 0x7e のエントリをコメントアウトする。 それぞれビルドして、
/usr/share/i18n/esdb/MISC/Shift_JIS.esdb
/usr/share/i18n/esdb/MISC/Shift_JIS-2004.esdb
/lib/libkiconv.so.4
を置き換えてOSを再起動すれば、0x7e 文字の変換が抑制されて、エラーが出なくなった。
「最」のエラー¶
mount_msdosfs の -L オプションに ja_JP.UTF-8 を指定してもほとんど問題はなかったのだが、 唯一、ファイル名に「最」の字が含まれているとエラーが出た。
条件と現象を記すと、 mount_msdosfs -D sjis -L ja_JP.UTF-8 でマウントし、 たとえば 最初.TXT が含まれるディレクトリに対して ls(1) を実行すると、
最初.TXT: No such file or directory.
とエラーになる。
原因を調べた結果、 /usr/src/sys/libkern/iconv_xlat16.c:iconv_xlat16_toupper()でおかしな挙動が 起こっていることが判った。
MS-DOSはファイル名の大文字小文字を区別せず、ファイル名は大文字で格納される。 そのためカーネルからMS-DOSファイルシステムにアクセスする時に大文字化処理をしていて、 上記関数はその処理をする関数である。この関数は20行程度の短い関数なので、 実際に中を見た方が理解が早い。
static int
iconv_xlat16_toupper(void *d2p, int c)
{
struct iconv_xlat16 *dp = (struct iconv_xlat16*)d2p;
int c1, c2, out;
if (c < 0x100) {
c1 = C2I1(c << 8);
c2 = C2I2(c << 8);
} else if (c < 0x10000) {
c1 = C2I1(c);
c2 = C2I2(c);
} else
return (c);
if (dp->d_table[c1] && dp->d_table[c1][c2] & XLAT16_HAS_UPPER_CASE) {
out = dp->d_table[c1][c2] & 0xffff;
if ((out & 0xff) == 0)
out = (out >> 8) & 0xff;
return (out);
} else
return (c);
}
見て判る通り、1バイト文字はビットシフトして下位バイトを 0 にして、 後で逆方向にビットシフトして元に戻す処理をしている。 そのため、下位バイトが 0 のマルチバイト文字は 1バイト文字に間違えられてしまうのだ。
ここまで書けば想像がつくと思うが、くだんの「最」の字は、UCS2 のコードが 0x6700 なのである。 そのためこの関数においては 0x67 つまり 'g' と認識され、これが大文字化して 0x47 'G' が 戻り値になる。
カーネルは G初.TXT というファイル名を探すが、ファイルシステムのディレクトリエントリには そのようなファイル名はもちろん存在しないため、ENOENT になる。
これを回避するため、私は以下のように修正した。
--- iconv_xlat16.c.orig 2018-06-22 08:02:52.000000000 +0900
+++ iconv_xlat16.c 2019-09-22 21:08:40.097838000 +0900
@@ -334,6 +334,8 @@
} else if (c < 0x10000) {
c1 = C2I1(c);
c2 = C2I2(c);
+ if (c1 == 0)
+ return (c);
} else
return (c);
この関数はカーネルモジュールの libiconv.ko に格納されるため、 libiconv.ko をリビルドして置き換えれば良い。 Makefile は /usr/src/sys/modules/libiconv/ にある。
予期せぬ副作用があるかも知れないので、mount_msdosfs を使用する時だけ libiconv.ko を入れ替えるようにする方が安全だろう。