ちうことで、さすがにバイナリ互換壊れたままは不味いもう一杯なので
メールかいたのですが
この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な文字が扱えなくなりますな。
そこは
理解していただいてるようでなにより。
さてさて、いろいろ
ガラガラ崩れはじめとりますが、もういっちょ救いようのない
問題を発見してしまいましたが、俺はもう限界かもしれない。
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
なにやら不吉なサブタイトルですが。
さて
前回は 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 と別なのに…どうしてこうなった…
昨日の補足再び、sh(1)でhistoryが切り詰められてしまう問題ですが、既に第3回でヒントが出てましたな。
el_gets(3)は IGNORE_EXTCHARS を使い el_wgets(3) の挙動を変更することで実装されてるという件。
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引数で返してる文字列の幅がワイド文字数のままのオカン。
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^)/
昨日の補足だけ。
どうにも春の頭痛胃痛ダブル祭りが絶賛開催中なもんでして…ちうことで
バッファリングとかするとバグらせる悪寒、俺に必要なのはバッファリンに違いない。
なんで余計なことは 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