The Man Who Fell From The Wrong Side Of The Sky:2013年7月22日分

[最新版] [一覧] [前月] [今月] [翌月]

2013/7/22(Mon)

[Citrus][pkgsrc] editors/vimでautodetectが有効にならない

@ vimというエディタではiconv(3)を文字コード判定に利用している

この話

ふつう

は大抵のISO-2022-JP実装に食わせれば不正なバイト列(=EILSEQ)扱いになることが多いのですが
Citrus iconvではそうはならないのですよね、既知の問題ではある(俺の中で)。

@ そもそもISO-2022-JPの仕様って

みんなみんな大嫌いISO-2022-JPは RFC1468で定義されてますが、以下のエスケープシーケンスで文字集合を切り替えます。

ではこれら以外のエスケープシーケンスが現れたとき、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
?$

となり 仕様で言うところの

に該当し、代替文字「?」で置換(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(=あ)」を喰わせると

となるわけです。そんで他の文字コードに変換しようとして代替文字に変換されるわけですな。

元々、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

ほげっ、何も帰ってこない…


*1:ISO-2022-CNだと「ESC $ ) A SI 〜 SO」ですが、ここではあくまで「ESC $ A」です、 ISO-2022-JP-2なんかを参照
*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)の実装がされていません。


[ホームへ] [ページトップへ]