○[NetBSD] wc: filename: invalid byte sequence
tech-userlevelネタ。
wizd(8)氏の添付したmp3ファイル(のようなもの)じゃde_DE.UTF-8ロケールでも再現しないねぇ。
ソース。
143 r = mbrtowc(wc, p, mblen, st);
144 if (r == (size_t)-1) {
145 warnx("%s: invalid byte sequence", file);
146 rval = 1;
147
148 /* XXX skip 1 byte */
149 mblen--;
150 p++;
151 memset(st, 0, sizeof(*st));
152 continue;
153 } else if (r == (size_t)-2)
154 break;
155 else if (r == 0)
156 r = 1;
そもそも*.mp3のよなバイナリをUTF-8だとしてmbrtowc(3)に喰わせれば
EILSEQになる可能性は高い、wc -cを使いましょうってことで。
↑の問題とは関係ないんだけど、今の実装だとmbrtowc(3)の使い方がちとマズイ。
注目は155~156行目、mbrtowc(3)の
仕様書には「戻り値として0が返ってきた場合
変換結果はnullワイド文字になる」と書いてある。
RETURN
0
If the next n or fewer bytes complete the character that corresponds
to the null wide character (which is the value stored).
なので
- r == 0 で条件分岐
- '\0'をスキップする為に r = 1をセットする
のはとても正しいように思えるんだけど、実際には
- mbrtowc(3)が戻り値に(size_t)0を返すのはnullワイド文字変換された場合だけではない
度々このチラシの裏にも書いてるけど
DR#288のケースがあるので
*wc == L'\0' の場合、 r == (size_t)0 は「真」
は正しいんだけれども、その逆は真ならずなのよね。
よって該当個所は
155 else if (*wc == L'\0')
と書き換える必要がある。
*1
- nullワイド文字に変換されるバイト列の長さは、stateful encodingの場合必ずしも1ではない
例えばISO-2022-JPなんかでは
L'あ' → { 0x1b, '$', 'B', '$', '"' }
L'\0' → { 0x1b, '(', 'B', '\0' }
となり、G0をasciiに切り替える為のescape sequenceが必要になる場合もあるわけ。
だから156行目の r = 1 の決めうちは正しくない。
修正方法は、mbrtowc(3)に1byteづつ喰わせる方法
r = mbrtowc(wc, p, 1, st); /* mblen分食わせるのでなく、1byteづつに制限する */
if (r == (size_t)-1) {
...
} else if (r == (size_t)-2)
break;
mblen--;
p++;
とするのが一番真っ当な修正方法。
しかーしこの方法だとmbrtowc(3)の呼び出しの回数が増え、性能が不利という向きには
r = mbrtowc(wc, p, mblen, st);
if (r == (size_t)-1) {
...
} else if (r == (size_t)-2)
break;
else if (*wc == L'\0') {
while (*p) {
p++;
mblen--;
}
r = 1;
}
...
mblen -= r;
p += r;
として、escape sequenceをすっ飛ばすインチキっぽい方法もある。
いちおうPOSIX localeには '\0' == L'\0' という縛り
*2があるので、これでもギリギリセーフだったりする。
もいっこバグ見つけた。
206 while ((len = read(fd, buf, MAXBSIZE)) > 0) {
207 if (dochar) {
208 size_t wlen;
209
210 r = do_mb(0, (char *)buf, (size_t)len,
211 &st, &wlen, name);
テキストファイルからread(2)で読み込んだバイト列に'\0'つかない時
iconv(1)のISO-2022-JP問題と同様の問題が発生する悪寒、ひー。
さてどうやって修正しますかねぇ…
Citrus API直接呼ぶのもアレなので
285 if (dochar && r == (size_t)-2) {
286 warnx("%s: incomplete multibyte character", name);
を
285 if (dochar && r == (size_t)-2 && !mbsinit(&st)) {
286 warnx("%s: incomplete multibyte character", name);
とかしてmbsinit(3)によるチェックくらいでお茶を濁す?
ここまでの
暫定patch。
まあテキストファイルであれば通常末尾にnewline(0xa)があることが多いんで
これを意図的に除去しない限りは
$ LC_ALL=ja_JP.ISO2022-JP wc -m hoge.txt
wc: hoge.txt: incomplete multibyte character
が発生することはないよなぁ、発生頻度は低そうなのでしばらく放置するかね。
*1:まあ問題になるのはVIQRとかzWなんかの7bit stateful encodingくらいだけど。
*2:なので'\0' != L'\0'でないUTF-16とかUTF-32のようなCESは扱うことができない。