📘

【PHP】mb_chr と mb_ord のレガシーエンコーディングに関する不適切な仕様

2023/10/11に公開

PHP 8.2 で mb_chrmb_ord を調べたところ、レガシーエンコーディングに関する不適切な仕様を見つけました。

mb_chr の第1引数および mb_ord の戻り値が Unicode のコードポイントになっており、内部の実装では Unicode とレガシーエンコーディングのあいだの変換処理を含みます。

この仕様は NEC や IBM の機種文字を扱うときに問題になります。例としてほぼ等しいをあらわす近似記号(≒、U+2252)を挙げます。Shift_JIS では2つの定義(0x81E0、0x8790)があります。

// U+2252: Approximately Equal to or the Image Of

var_dump(
    "\x81\xE0" === mb_chr(0x2252, 'cp932'),
    0x2252 === mb_ord("\x87\x90", 'cp932')
);

mb_chrU+2252 を指定すれば \x81\xE0 は生成できますが、\x87\x90mb_chr から直接生成できません。Shift_JIS の文字をすべて生成するテストをする際にはコードポイントから文字を生成する関数を自前で実装しなければなりません。

\x87\x90 から U+2252 のコードポイントを求めることはできますが、前述のとおり mb_chrU+2252 から \x87\x90 を生成することはできません。

Shift_JIS の大文字小文字変換や空白文字の除去など、文字とコードポイントのあいだの変換処理を PHP のコードで実装する場合、仕様で想定しない文字まで変換するバグを抱えることになります。

実際、mb_scrubmb_strtolowermb_strtouppermb_convert_case は機種文字を変換してしまうバグを抱えています。

var_dump(
    "\x81\xE0" === mb_scrub("\x87\x90", 'cp932'),
    "\x81\xE0" === mb_strtolower("\x87\x90", 'cp932'),
    "\x81\xE0" === mb_strtoupper("\x87\x90", 'cp932'),
    "\x81\xE0" === mb_convert_case("\x87\x90", MB_CASE_TITLE, 'cp932')
);

私が mb_chrmb_ord をコミットした時点(2016年)では Unicode との変換を含まない仕様になっていたのですが、知らないうちに仕様が変わっていました。近いうちに PHP の Github の Issue を立てて開発者と協議します。

Discussion