[PHP-users 12952] Re: wddxについて(解決)

Reiji Matsumoto php-users@php.gr.jp
Tue, 4 Feb 2003 02:31:50 +0900


phpとNetBSDのソースを眺め、ようやく問題の全貌を把握できました。

まず第1に、php 4.2.2 の wddx.cには明らかなバグがある事が分かりました。
第2に、NetBSDの"ja_JP.eucJP"の実装にも問題がある事が分かりました。

■回避方法
まず最初に回避方法から説明させて頂きます。
php 4.2.2 のソースの一部を書き換え、リコンパイルする必要があります。
その際、

[php-4.2.2の展開ディレクトリ]/ext/wddx/wddx.c

を書き換えます。

wddx.c:394: if (iscntrl((int)*p)) {

を、

wddx.c:394: if (iscntrl((int)(*(unsigned char*)p))) {

と、します。これで wddx.cによるメモリの不正参照(出力が揺れる原因。後述)
を回避できます。
次に、NetBSDの"ja_JP.eucJP"ロケールには問題があるため、wddxを利用
する時は、常に"C"ロケールを使うようにします。
よってテストプログラムは以下のようになります。

<?php
$locale = setlocale(LC_ALL,"C"); // ja_JP.eucJP ロケールを使わない
$a = "赤黒";
$b = wddx_serialize_value($a);
print "locale=[{$locale}]<br><xmp>$b</xmp>";
?>

Red Hat Linux 7.0.1J および NetBSD1.6 で動作確認をしました。

■出力が揺れる理由
予想通り、NetBSD のiscntrl(3)で参照している unsigned char *_ctype_ は
257バイトの配列でした。そしてsetlocale(3)を実行する時に、ロケールを
参照しながら_ctype_に適切な値をコピーする実装になっています。
_ctype_のメモリ確保は

[NetBSDのソース]/src/lib/libc/locale/runeglue.c

で行われており、その実装は以下の通りです。

runeglue.c:90: new_ctype = malloc(sizeof(*new_ctype) * (1 +
_CTYPE_NUM_CHARS));
runeglue.c:148: _ctype_ = (const unsigned char *)new_ctype;

負の領域は一切確保しておらず、_CTYPE_NUM_CHARSは256ですので、有効範囲は
オフセット0からオフセット256まででした。よって、

wddx.c:394: if (iscntrl((int)*p)) {

では負の領域を参照してしまうため、出力が"赤黒"になったり"<char code=..."
になったりしていたようです。
iscntrl(3)の仕様が

>  これらの関数は c をチェックして、現在のlocaleに従い分類す
>  る。この c は unsigned char か EOF でなければならない。

であるなら、問題があるのはwddx.cの方であり、こちらを書きかえる事にしました。

■"ja_JP.eucJP"ロケールを使わず、"C"ロケールを使う理由
これですべての問題が解決できればよかったのですが、実際にはまだ問題が
ありました。上記のようにwddx.cを書き換え"ja_JP.eucJP"ロケールを利用する
と、"赤"は正しく表示されるのですが、"黒"が正しく表示されなくなってしま
いました。
そこでそれぞれのロケールを利用した時の_ctype_の中身をダンプしてみると、
以下のようになっていました。

"C"ロケールの場合
+01|20:20:20:20:20:20:20:20:20:28:28:28:28:28:20:20
+11|20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20
+21|88:10:10:10:10:10:10:10:10:10:10:10:10:10:10:10
+31|04:04:04:04:04:04:04:04:04:04:10:10:10:10:10:10
+41|10:41:41:41:41:41:41:01:01:01:01:01:01:01:01:01
+51|01:01:01:01:01:01:01:01:01:01:01:10:10:10:10:10
+61|10:42:42:42:42:42:42:02:02:02:02:02:02:02:02:02
+71|02:02:02:02:02:02:02:02:02:02:02:10:10:10:10:20
+81|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+91|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+A1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+B1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+C1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+D1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+E1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+F1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
※+00はEOFのための値なので省略

"ja_JP.eucJP"ロケールの場合
+01|20:20:20:20:20:20:20:20:20:28:28:28:28:28:20:20
+11|20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20
+21|88:10:10:10:10:10:10:10:10:10:10:10:10:10:10:10
+31|44:44:44:44:44:44:44:44:44:44:10:10:10:10:10:10
+41|10:41:41:41:41:41:41:01:01:01:01:01:01:01:01:01
+51|01:01:01:01:01:01:01:01:01:01:01:10:10:10:10:10
+61|10:42:42:42:42:42:42:02:02:02:02:02:02:02:02:02
+71|02:02:02:02:02:02:02:02:02:02:02:10:10:10:10:20
+81|00:00:00:00:20:20:20:20:20:20:20:20:20:20:20:20
+91|20:20:20:20:20:20:20:20:00:00:00:20:20:20:20:20
+A1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+B1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+C1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+D1|00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
+E1|20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20
+F1|20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:00
※+00はEOFのための値なので省略

制御コードかどうかは0x20との論理積で判断するわけですが、"ja_JP.eucJP"
の場合、文字コード0x84~0x97、0x9B~0x9F、0xE0~0xFE が制御コードとして
定義されています。よって"赤"は"C0 D6"ですので問題なく表示できますが、
"黒"は"B9 F5"で"F5"が制御コードとして判断されてしまうため、正しい出力
ができない状態でした。
本来ならmklocale(1)を利用し正しいロケールを作るべきだと思うのですが、
このツールの使い方がよく分からず今回はそこまで試していません。
ただしphpのwddxに限って言えば"C"ロケールで問題ない事を(良くも悪くも
実装と照らし合わせて)確認したので、

wddxを使う時は"C"ロケールを使う

という事にしました。

■考察
今回の問題は wddx.c におけるメモリの不正参照、およびNetBSD1.6のロケールの
実装という2つの問題が絡みあい、出力が揺れるという極めて良くない結果
を生みだしていたようです。
今回はwddx.cのソースの一部を書き換え、ja_JP.eucJPロケールを利用しない事で
回避しました。しかし実際にはja_JP.eucJPロケールを正しく実装しなくてはなら
ないし、wddx.c も、もう少し丁寧な処理を行うように書き直さなくてはならない
ように思いました。現在、wddx.c では入力として与えられた文字列を1バイト
ずつインクリメントしながら1バイトずつチェックしています。マルチバイト
文字列の1バイト目も2バイト目も一切区別していません。また、'<'、'>'、'&'
等、xmlデータとしてそのまま配置できない文字を例外処理していますが、その
場合でもマルチバイト文字列の2バイト目が'<'、'>'、'&'であるような場合を
考慮していません。現実的にはそういう文字セットは存在しない(あるいは日本
では一般的でない?)ため表面化しませんが、本来ならロケールと照らし合わせな
がら確認していかなければならない事だと思います。
※UTF-16の場合は別な処理を行うらしいので問題ないようです

■その他
> LC_ALL が ja_JP.eucJP の時はいろいろ戻ってますね。

NetBSD1.6のsetlocale(3)の実装では、第1パラメーターにLC_ALL、
第2パラメーターにNULLが指定されていた時、LC_COLLATE、LC_CTYPE、
LC_MONETARY、LC_NUMERIC、LC_TIME、LC_MESSAGESの6つの値を、今羅列
した順番で'/'で区切って出力する仕様になっていました。
なお、これもソースを見て知ったのですが、実際にロケール処理が実装
されているのはLC_CTYPEとLC_MESSAGEだけでした。
それ以外のロケールは常に"C"になるようです。ですのですべての
ロケールをja_JP.eucJPにしようとして、

setlocale (LC_ALL, 'ja_JP.eucJP');

を実行しても、

> C/ja_JP.eucJP/C/C/C/ja_JP.eucJP

と出力されてしまうようです。
NetBSDのロケールサポートはまだまだみたいですね。


Matsumoto@Sp