2008年3月23日の日記を表示中
2008年 3月23日 (日)
■Windows版Pidginの文字コード変換
Windows版のPidginの oscar (AIMとかICQのプロトコル) では,ユーザの入力したUTF-8な文字列を UTF-16BEに変換する前に,まず ISO-8859-1 に変換できるか試します.これで変換できたら1byte文字で送ったりするわけで,多分常にUTF-8で送るよりは多少は効率的とか,そんな感じなんでしょう (oscarでは非Latin1な文字はUTF-16BEで送るらしいので).
で,Windowsだと,上記の箇所で,全角記号や全角英数字までもが UTF-8 から ISO-8859-1 へ変換できてしまい,しかもその際勝手に半角に置き換えられてしまう・・・というのが今回の問題.上の方でworkaroundを仕込むのもいいんですが,気持ちが悪いのでどうにかできんかと,yazさんを無理矢理巻き込みつつ原因を追ってみました.以下,わかったことと推測されることの要点.
- g_convertを呼んで UTF-8 の全角アルファベットをISO-8859-1に変換するだけのコードを書いてテストしたところ,Linuxでは g_convert に失敗するが,Windows上でPidginについてきた Glib とリンクさせると成功して半角アルファベットになった.Pidginが変なことをしているわけではない模様.
- GNU libiconvには,変換先の文字コードに「ISO-8859-1//TRANSLIT」のように「//TRANSLIT」をつけると,前述のような挙動になる機能があった (libiconvのソースのlib/translit.defを見てみよう).
- Windows版のPidginと一緒に入るGTKのGlibは,なんと GNU libiconvを使っておらず,代わりに win_iconvというのを利用している模様 (libglib-2.0-0.dll を objdump -d して確認).glib-2.16.1 のソースを落として中を見てみたら,普通に win_iconv 入ってるのね.
- win_iconv は,Windows のAPIを呼び出してlibiconv互換のAPIを実現しているっぽい.多分 UTF-8 から ISO-8859-1 に変換する際には,WideCharToMultiByte が呼ばれることになる.
- WideCharToMultiByte は,デフォルトで「TRANSLIT」的な動作をするようで,2個目の引数に「WC_NO_BEST_FIT_CHARS」というフラグをつけるとこれを防げるらしい (ここに書いてある「WC_NO_BEST_FIT_CHARS」の説明だと逆なように見えるのでいまいち確証が持てませんが).
- Glib内蔵の win_iconv のソースを見ると,2個目の引数に0が突っ込まれていた.外からいじれないっぽい (´・ω・`)
というわけで,アプリの側からどうこうできるような問題じゃないっぽいというのが結論でございます.これだけ書くとすごくあっさりここにたどり着いたように見えますが,どこで問題が起きているのかなかなか見抜けず,半日ずっとこればっかやってました _|‾|○
で,既存のコードを尊重するなら,以下のようなパッチが良さそうです.ISO-8859-1に変換できたとしてもすぐに結果を信じずに,再変換してサイズが同じだったら初めて信じる,みたいな.
--- /home/compile/pidgin-2.4.0/libpurple/protocols/oscar/oscar.c.old +++ /home/compile/pidgin-2.4.0/libpurple/protocols/oscar/oscar.c @@ -551,7 +551,19 @@ * XXX - We need a way to only attempt to convert if we KNOW "from" * can be converted to "charsetstr" */ - *msg = g_convert(from, -1, charsetstr, "UTF-8", NULL, &msglen, NULL); + *msg = g_convert(from, strlen(from), charsetstr, "UTF-8", NULL, &msglen, NULL); +#ifdef _WIN32 + if (*msg != NULL) { + gchar *msgr; + msgr = g_convert(*msg, strlen(*msg), "UTF-8", charsetstr, NULL, NULL, NULL); + if (msgr == NULL || strcmp(msgr, from) != 0) { + g_free(*msg); + *msg = NULL; + } + if (msgr != NULL) + g_free(msgr); + } +#endif if (*msg != NULL) { *charset = AIM_CHARSET_CUSTOM; *charsubset = 0x0000;
古いクライアントなんてクソ喰らえなら,こんな数byteをケチるためのコードとはとっととお別れしてUTF-16BEで統一すべきですね(笑).って,ICQはそれで平気なんだっけ?
2008年3月23日の日記を表示中
[コメントを書く]