Not only is the Internet dead, it's starting to smell really bad.:2010年03月上旬

2010/03/01(Mon)

[NetBSD] libedit I18N への道(その5)

昨日の補足だけ。

どうにも春の頭痛胃痛ダブル祭りが絶賛開催中なもんでして…ちうことで
バッファリングとかするとバグらせる悪寒、俺に必要なのはバッファリンに違いない。
なんで余計なことは libc にお任せの高水準API使って書き直しちゃるぞーということで。

今回のケースでは元々動作が怪しいので尊重すべきは
ワイド文字対応コードが入る前のソースであるからして、すべてバックアウトした状態
つまり rev1.30をベースに作業することにしますか。

その1、vi(1)に渡す一時ファイルを作成する部分のオリジナル。

1013 	fd = mkstemp(tempfile);
1014 	if (fd < 0)
1015 		return CC_ERROR;
1016 	cp = el->el_line.buffer;
1017 	write(fd, cp, (size_t)(el->el_line.lastchar - cp));
1018 	write(fd, "\n", 1);
1019 	pid = fork();

説明の必要ありませんね?

書き直したコード。

1016 	fd = mkstemp(tempfile);
1017 	if (fd < 0)
1018 		return CC_ERROR;
1019 	fp = fdopen(fd, "w+");
1020 	if (fp == NULL) {
1021 		close(fd);
1022 		return CC_ERROR;
1023 	}
1024 	for (cp = el->el_line.buffer; cp < el->el_line.lastchar; ++cp) {
1025 		fputwc(*cp, fp);
1026 		if (ferror(fp))
1027 			goto fatal;
1028 	}
1029 	fputwc(L'\n', fp);
1030 	if (ferror(fp))
1031 		goto fatal;
1032 	fflush(fp);
1033 	pid = fork();
1034 	switch (pid) {
1035 	case -1:
1036 		goto fatal;
1037 	case 0:
1038 		fclose(fp);
1039 		execlp("vi", "vi", tempfile, (char *)NULL);
1040 		exit(0);
1041 		/*NOTREACHED*/

という感じ。

その2、vi(1)が編集した一時ファイルを読み込む部分のオリジナル。

1031 		while (waitpid(pid, &status, 0) != pid)
1032 			continue;
1033 		lseek(fd, (off_t)0, SEEK_SET);
1034 		st = read(fd, cp, (size_t)(el->el_line.limit - cp));
1035 		if (st > 0 && cp[st - 1] == '\n')
1036 			st--;
1037 		el->el_line.cursor = cp;
1038 		el->el_line.lastchar = cp + st;
1039 		break;

説明の(ry

書き直し(ry

1043 		while (waitpid(pid, &status, 0) != pid)
1044 			continue;
1045 		unlink(tempfile);
1046 		rewind(fp);
1047 		for (cp = el->el_line.buffer; cp < el->el_line.limit; ++cp) {
1048 			ch = fgetwc(fp);
1049 			if (ferror(fp))
1050 				goto fatal;
1051 			if (feof(fp))
1052 				break;
1053 			*cp = ch;
1054 		}
1055 		if (cp != el->el_line.buffer && *(cp - 1) == L'\n')
1056 			--cp;
1057 		el->el_line.cursor = el->el_line.buffer;
1058 		el->el_line.lastchar = cp;
1059 		break;

あとは-DWIDECHARなしでbuildする場合のためにchartype.hに

#ifdef WIDECHAR

#define ct_fgetwc	fgetwc
#define ct_fputwc	fputwc

#else

#define ct_fgetwc	fgetc
#define ct_fputwc	fputc

#endif

としときましょう。

ただしオリジナルのコードと比較すると -UWIDECHAR の場合には read/write 一発でなく
fgetc/fputc の複数回の呼び出しになるので system call の呼び出し回数は libc が
適切にバッファリングでセーブするとはいえ、ライブラリ関数の呼び出し回数が今度は増えちゃいますやね。
もし性能にシビアな要求がある場合は、vi.c 側で ifndef WIDECHAR の場合は fread/fwrite を
使うことも検討する必要があります、まぁ今回はせいぜい一行(1024文字)までなんでやらんけど。

とりあえずこれでLANG=ja_JP.eucJPで起動したsh(1)でset -o viし、ESC + v押下で
historyをvi(1)上から編集できるのですがvi_histedit()から先でバグってるようで
マルチバイトの場合、実行されるhistoryが不正に切り詰められちゃってる模様。
多分ワイド文字数とバイト数がどっかでごっちゃなんだろうなー。

[C locale の場合]
vmware$ LANG=C sh
vmware$ set -o vi
vmware$ <ESC> v
\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa
~
~
~
:w!
vmware$
あいうえお: not found
vmware$

[ja_JP.eucJP locale の場合]
vmware$ LANG=ja_JP.eucJP sh
vmware$ set -o vi
vmware$ <ESC> v
あいうえお
~
~
~
:w!
vmware$
あいう: not found
vmware$

現時点での差分は こちら

次回はこの切り詰め問題を追っかける予定ですがいつもの通り未定 :D

2010/03/02(Tue)

[NetBSD] libedit I18N への道(その5.1)

昨日の補足再び、sh(1)でhistoryが切り詰められてしまう問題ですが、既に第3回でヒントが出てましたな。
el_gets(3)は IGNORE_EXTCHARS を使い el_wgets(3) の挙動を変更することで実装されてるという件。

eln.c

72 public const char *
73 el_gets(EditLine *el, int *nread)
74 {
75 	const wchar_t *tmp;
76
77 	el->el_flags |= IGNORE_EXTCHARS;
78 	tmp = el_wgets(el, nread);
79 	el->el_flags &= ~IGNORE_EXTCHARS;
80 	return ct_encode_string(tmp, &el->el_lgcyconv);
81 }

ここで第2引数で返してる文字列の幅がワイド文字数のままのオカン。

read.c

504 		*nread = (int)(el->el_line.cursor - el->el_line.buffer);
505 		goto done;

ズコー、IGNORE_EXTCHARS をセットした意味ないじゃんよこれwww
まー el_line ではワイド文字で持ってるんで、どうしても el_wgets(3) の中では計算できないのは確か。

手堅く修正するのであれば ct_encode_string() のIFを弄って、ワイド文字→マルチバイトに変換された
バイト数を返すように修正する鹿、なのですがもう正直 eln.c を一から書き直したい衝動がゴゴゴゴゴ。

まぁ本格対応の前に暫定fixを入れてみる。

Index: eln.c
===================================================================
RCS file: /cvsroot/src/lib/libedit/eln.c,v
retrieving revision 1.6
diff -u -r1.6 eln.c
--- eln.c       20 Jan 2010 01:15:52 -0000      1.6
+++ eln.c       2 Mar 2010 14:54:17 -0000
@@ -73,11 +73,15 @@
 el_gets(EditLine *el, int *nread)
 {
 	const wchar_t *tmp;
+	const char *s;

 	el->el_flags |= IGNORE_EXTCHARS;
 	tmp = el_wgets(el, nread);
 	el->el_flags &= ~IGNORE_EXTCHARS;
-	return ct_encode_string(tmp, &el->el_lgcyconv);
+	s = ct_encode_string(tmp, &el->el_lgcyconv);
+	if (nread != NULL)
+		*nread = strlen(s);
+	return s;
 }

これで実行してみると

vmware$ LANG=ja_JP.eucJP sh
vmware$ set -o vi
vmware$ <ESC> v
あいうえお
~
~
~
:w!
vmware$
あいうえお: not found
vmware$

うまくいきました\(^o^)/

2010/03/05(Fri)

[NetBSD] libedit I18N への道(その5.2)

@ふりだしに戻る

なにやら不吉なサブタイトルですが。

さて 前回は el_gets() の返す文字幅がバイトでなくワイド文字になってる件の対策で
ct_encode_string() のインタフェースを変更し、ワイド文字→マルチバイトの変換で
何バイト変換されたかを返すようにすればいいんじゃね?という結論になりましたが
これには実は巨大な落とし穴があります。

なぜなら el_gets() はそもそも内部で mbrtowc(3) のたぐいは使ってはならないのです。
これは 大惨禍い第3回で el_getc() の実装について説明した理由とそのまんま同じです。
現状のコードでは、LANG=ja_JP.eucJP で { 0xA1, 0x41, 0x0 } みたいなマルチバイトを喰わせると
これをワイド文字に変換しようとして変換エラーで EILSEQ になってしまうのですが
el_gets() はあくまでバイト志向の API なのでこれは許されないのですよ、うーアボガドバナナ。


いう
 こと
   は

この問題はかなり致命的です、現在 editline 構造体において一行分のデータは
el_line フィールド、すなわち el_line_t 構造体で管理しているのですが

74 typedef struct el_line_t {
75 	Char	   *buffer;        /* Input line                   */
76 	Char	   *cursor;        /* Cursor position              */
77 	Char	   *lastchar;      /* Last character               */
78 	const Char *limit;         /* Max position                 */
79 } el_line_t;
...
116 struct editline {
...
128 	el_line_t         el_line;      /* The current line information */
...

ちゅうように、すべて Char(=wchar_t) すなわちワイド文字として行情報を保持してやがるのです。
ですのでマルチバイト→ワイド文字変換をしない限り、行情報を保持できないという。困った。

こりゃ完全に設計ミスですな、ちうことは この変更は全部 backout して元に戻さんとならん悪寒。

/(^o^)\ナンテコッタイ

histedit.h では LineInfo と LineInfoW と別なのに…どうしてこうなった…

2010/03/06(Sat)

[NetBSD] libedit I18N への道 (その5.3)

@この patch は出来そこないだ。食べられないよ by 山岡士郎

さてさて、いろいろ ガラガラ崩れはじめとりますが、もういっちょ救いようのない
問題を発見してしまいましたが、俺はもう限界かもしれない。

editline(3)には以下の機能があります。

     el_set()
           Set editline parameters.  op determines which parameter to set, and
           each operation has its own parameter list.

           The following values for op are supported, along with the required
           argument list:

           EL_GETCFN, int (*f)(EditLine *, char *c)
                 Define the character reading function as f, which is to
                 return the number of characters read and store them in c.
                 This function is called internally by el_gets() and
                 el_getc().  The builtin function can be set or restored with
                 the special function name ``EL_BUILTIN_GETCFN''.

要するに tty(4) から文字(列)を読み込むための関数を override できる機能なのです。
EL_BUILTIN_GETCFN というのは正に 第3回でいじった read_char() そのものです。

さっそく el.c のコードをみてみませう。

281 	case EL_GETCFN:
282 	{
283 		el_rfunc_t rc = va_arg(ap, el_rfunc_t);
284 		rv = el_read_setfn(el, rc);
285 		el->el_flags &= ~NARROW_READ;
286 		break;
287 	}

el_set(3)の引数は可変引数リストですので、283行目で va_arg(3) を使って取得します。
ここでは override するint (*f)(Editline *, char *)型の関数ポインタは
read.h で typedef される el_rfunc_t となっとるのですが、で・す・が。

@@ -35,7 +35,7 @@
 #ifndef        _h_el_read
 #define        _h_el_read

-typedef int (*el_rfunc_t)(EditLine *, char *);
+typedef int (*el_rfunc_t)(EditLine *, Char *);

 typedef struct el_read_t {
        el_rfunc_t      read_char;      /* Function to read a character */

ぎゃー勝手にインタフェースを char -> Char(=wchar_t) に変更してやがんの。
これはひどい、ひどすぎる。

お分かりだと思いますが、これバイナリの後方互換をパーペキに失ってます。
後方互換を失うって何?という人は 以前の記事読んでください。

ですので古いバイナリが int (*)(EditLine *, char *) の関数ポインタを新しい libedit に渡してしまい
int (*)(EditLine *, wchar_t*)として呼ばれたら、まずまともに動かないことでしょう。

ま唯一の救いは、新しい libeditは char よりも大きな wchar_t のサイズを持つ
記憶領域へのポインタを古いバイナリ側が用意した override 関数に渡すので
まずbuffer overflow は発生しないだろうということくらいっすか。
# つったって、(この機能を使ってないとはいえ)sh(1)とかちょー重要なアプリがリンクするライブラリでこれはねーよ…

ということで、次回からは1月の変更をすべて Reset / Mutemathして(笑)
libedit を一から i18n 化する連載ということでwwwちょwww

2010/03/07(Sun)

[NetBSD] libedit I18N への道 (その5.3.1)

@

ちうことで、さすがにバイナリ互換壊れたままは不味いもう一杯なので メールかいたのですが
このpatchをcommitした christos氏曰く 大丈夫じゃね?と。

というのも、前回書いた話

ま唯一の救いは、新しい libeditは char よりも大きな wchar_t のサイズを持つ
記憶領域へのポインタを古いバイナリ側が用意した override 関数に渡すので 

正にこの事実を逆に利用して、el_wgetc(3) の中で

420         num_read = (*el->el_read.read_char)(el, cp);
421 #ifdef WIDECHAR
422         if (el->el_flags & NARROW_READ)
423                 *cp = *(char *)(void *)cp;
424 #endif

として、wchar_t を char に整形してたという、汚いなさすが kludge きたない

この NARROW_READ flag は el_set(3) の中でセットしてまんな。

        /* XXX: do we need to change el_rfunc_t? */
        case EL_GETCFN:         /* el_rfunc_t */
                ret = el_wset(el, op, va_arg(ap, el_rfunc_t));
                el->el_flags |= NARROW_READ;
                break;


そもそも el_set(3) があるのに el_wset(3) とか必要ねーだろ説。
それにdo we needとか疑問形にせんでも、EL_WGETCFN を追加して
ワイド文字版の el_rfunc_t を別定義するが普通だと思います、まぁもう手遅れですが。

めんどくさくなったのでもうバイナリ互換OKならなんでもいいや。

しかしそもそも現在 EL_GETCFN のデフォルト値である EL_BUILTIN_GETCFN(=read_char)
を置き換えられちゃうと、マルチバイト→ワイド文字の変換をやっとるのはここなので
el_set(EL_GETCFN) されると el_wgetc(3) でまともにMB_CUR_MAX > 1な文字が扱えなくなりますな。
そこは 理解していただいてるようでなにより。