2013/07/22(Mon)
○[Citrus][pkgsrc] editors/vimでautodetectが有効にならない
@vimというエディタではiconv(3)を文字コード判定に利用している
この話。
ふつう
- eucJPの「あ」、バイト列だと「\xa4\xa2」
- Windows-31Jの「あ」、バイト列だと「\x82\xa0」
は大抵のISO-2022-JP実装に食わせれば不正なバイト列(=EILSEQ)扱いになることが多いのですが
Citrus iconvではそうはならないのですよね、既知の問題ではある(俺の中で)。
@そもそもISO-2022-JPの仕様って
みんなみんな大嫌いISO-2022-JPは RFC1468で定義されてますが、以下のエスケープシーケンスで文字集合を切り替えます。
- ESC(B … ISO646-US(US-ASCII)
- ESC(J … ISO646-JP(JIS X0201:1976-right)
- ESC$B … JIS X0208:1983 or 1990
- ESC$@ … JIS X0208:1978
ではこれら以外のエスケープシーケンスが現れたとき、iconvはどういう動作をするでしょうか?
例としてISO-2022-JPには存在しない「ESC $ A」つまりGB2312を指示するエスケープシーケンス *1をいくつかのiconv実装に喰わせてみましょう。
まずGNU libiconvの場合
$ printf '\x1b$A!!\x1b(B' | /usr/pkg/bin/iconv -f iso-2022-jp -t utf-8
/usr/pkg/bin/iconv: (stdin):1:0: cannot convert
EILSEQとして変換が停止します。
一方Citrus iconvの場合
$ printf '\x1b$A!!\x1b(B' | /usr/bin/iconv -f iso-2022-jp -t utf-8
iconv: warning: invalid characters: 1
?$
となり 仕様で言うところの
- 変換元の文字コードとしては正しいバイト列(the input buffer that is valid)
- 変換先のUTF-8/UCSに対応する文字がない(an identical character does not exist in the target codeset)
に該当し、代替文字「?」で置換(perform an implementation-defined conversion on this character)して出力されています。
@なぜこの違いは生まれるのか?
おそらくGNU libiconvではRFC1468をstrictに解釈し実装した結果、未知のエスケープシーケンスがきたらEILSEQを返すのでしょう。
こういう実装が大多数と思われます、しかし果たしてそれは正しいことなのでしょうか?
一方でCitrus iconvでは、CES層では未知のエスケープシーケンスが現れた場合、それが
ISO/IEC 2022として正しければ貪欲に変換を実行します。
なぜならISO-2022-JPはISO/IEC 2022のサブセット
*2であり、バイト列としては完全に正常だからです。
なんせCCS層においては、将来のバージョンで新たなエスケープシーケンスが追加される可能性がありますし。ちゅーか実際に
とかありましたが、まぁ気にするな。
@元来iconv(3)やmbrtowc(3)は入力バイト列は緩く解釈するのが普通であった
将来的な拡張のため、実際には文字が割り当てられてなくても不正なバイト列として
これを扱わないのはごく普通に行われてることで、これはISO-2022-JPに限った話ではありません。
例えばみんな大好きUTF-8ですが、
RFC3629において禁止されるまではUTF-8の5~6byte目は有効なバイト列でした。
また
ISO/IEC 10646においてU+110000つまり17面以降を私用領域として使ってた古い実装との関係上、未だに有効と考えたほうがいいでしょう。
このことはRFC3629にも4byteまでと決め付けるのはbuffer overflowのリスクがあると説いてます。
ですので実際に文字が割り当てられてなくても有効なバイト列であるという解釈は、UTF-8にも当てはまる事情となります。 *3。
そしてこれはUnicode、ISO/IEC 10646やISO/IEC 2022だけの問題ですらありません。
大抵の文字集合
*4には空きスペースがあって、改訂のある度にこれが埋まっていきます。
この空きスペースを予約領域として、有効なバイト列として扱うのは普通に行われてることです。
ところが、UCS Normalizationなwchar_tを持つようなlibc実装においては、これらの空きスペースを緩く扱うことは不可能です。
常に厳しくEILSEQにせざるをえません、やっぱウンコだなぁ。
@話を元に戻すと
Citrus iconvにさっきのeucJPの「\xa4\xa2(=あ)」やWindows-31Jの「\x82\xa0(=あ)」を喰わせると
- ISO/IEC 2022においては、これらはC1/GRであり正常なバイト列である
- ただしISO-2022-JPではC1/GRには文字集合を指示していないので、対応する文字は存在しない
となるわけです。そんで他の文字コードに変換しようとして代替文字に変換されるわけですな。
元々、Citrus iconvのISO/IEC 2022モジュール(
citrus_iso2022.c)は、
nvi-m17nのISO/IEC 2022モジュール(multi_iso2022.c)を
作者のitojun氏が直々に、
4.4BSD runeのAPI(sgetrune/sputrune)に合わせて書き直したものがベースとなっています。
ですので最初からISO/IEC 2022の完全実装
*5を目指してスタートしてるので、そこらのISO-2022-JPだけ実装しました的なモノとは違うのですよ。
@結論
つまりはだ、結局iconv(3)で文字コードのautodetectをやろうと思ったらillegal byte sequenceのケースだけでなく
no corresponding characterのケースもトラップしないとならないわけで、いつもの
GNU libiconvのPOSIX違反動作にでも依存するしかないわけです。
そしてそのPOSIX違反動作を回避しようとすると更なる 移植性問題と、トラブルの種にしかならんのですよ。
こういう本来使うべきでない用途にiconv(3)を使うのはホント勘弁して欲しいです。
サイズも形状も合わないドライバーでネジ回したら、ネジ山をなめて壊してしまうだけです。
それは正しいプログラマの態度ではなく、ただの横着モノであり災厄を呼び寄せる元です。
ちなみに nvi2を作業してる学生さんだかは、file(1)を使ってautodetectを実装しようとしてた記憶。
$ file eucJP.txt
eucJP.txt: ISO-8859 text
$ file cp932.txt
cp932.txt: Non-ISO extended-ASCII text
役に立たんけど。
@おまけ
ちなみにglibc2のiconvでは
$ printf '\x1b$A!!\x1b(B' | iconv -f iso-2022-jp -t utf-8
ESC$A!!
なんじゃこりゃ、未知のエスケープシーケンスはまんまUTF-8のバイトに変換するのか…
perl付属のpiconvでは
$ printf '\x1b$A!!\x1b(B' | piconv -f iso-2022-jp -t utf-8
ほげっ、何も帰ってこない…
*2:まぁ厳密には非互換性あるんですが…
*3:ちなみにCygwinは古くからUTF-8は4byte目までしか許容してないのですが、あれはmbstate_tをよく考えずにglibc2からコピペした事による設計ミスであって、UTF-8 decoderにはXXXのコメントがあります。
*4:94/96文字集合のような小さなものから、CNS11643やGB18030のような巨大な文字集合まで、事情は変わりませんな。
*5:現時点ではESC % @によるUTF-8の指示など、DRCS(dynamically redefinable character sets)の実装がされていません。