てきとうなさいと べぇたばん

非公式&永遠の調査中 PHP 8.1のmbstringアップグレードガイド

TOP > てきとうにこらむ > ゲーム作りとプログラミング日記 > 非公式&永遠の調査中 PHP 8.1のmbstringアップグレードガイド

GitHub上のMajor overhaul of mbstringのプルリクエスト

PHP 8.1へのアップグレードにまつわるまとめ

PHP 8.1へのアップグレードには、mbstringにまつわるマニュアルに記述されない後方互換性のない変更が含まれることがあります。そのことを周知するべく、この記事を書くことにしました。

私てきめんは、PHPカンファレンス 2022にて、「治っていくmbstring 令和時代の文字化け」というタイトルでトークしています。以下スライドも参考にしてください。

Major overhaul of mbstringについて

PHP 8.1から、Major overhaul of mbstringと呼ばれる、mbstringの大規模改修の内容が反映されるようになりました。困ったことに、RFC(Request For Comments)やChangelog、マニュアルにない内容で、mbstringを多用するPHPユーザーにとてつもない困惑をもたらすこととなりました。もちろん、バグ修正や高速化などの恩恵も受けられるのですが、後方互換性が失われてはアップグレードを躊躇することになるでしょう。

後方互換性がなさそうな挙動

mbstringはlibmbflというライブラリからなっていて、それをPHPがメンテナンスしているため、全体的にmbstring関数は後方互換性のない変換をする可能性があります。以下は既知の影響になります。

mb_convert_encodingによる、違う文字への変換

以下のコードのように、文字コードを変更する関数、mb_convert_encoding関数によって、違う文字に変換される可能性があります。

mb_convert_encoding("~", "UTF-8", "SJIS");

このケースでは、PHP 8.1.7まで「~」が返ってきます。これは、JIS X 0201からすると正しい変換なのですが、PHP 8.0以前では「~」が返ってきます。このように、後方互換性が考えられていないため、いきなり変換されてびっくりすることでしょう。このことを指摘したため、後方互換性が重視されて「~」にPHP 8.1.8以降戻されることとなりました。個人的に、こういうのはRFCを挟んでほしいです。

mb_substitute_characterが突如現れることがある

以下のコードを見てください。

mb_convert_encoding("a", "UTF-8", "UTF-16");

PHP 8.0までは「」(空文字)が返ってきますが、PHP 8.1からは「?」が返ってきます。これは、mb_substitute_characterの設定を反映していることになります。たしかに、$from_encodingにUTF-16を設定していますから、"a"は不正な文字です。しかし、PHP 8.0まではなぜか空文字を返していて、mb_substitute_characterの設定を無視していたようです。PHP 8.1ではmb_substitute_characterの設定の影響を受けることとなります。しかし、これがUTF-16限定の話なのか、他の文字エンコーディングにも及んでいるのかは調査中です。

mb_strwidthの数え方が変わった

以下のコードのような、ZWJ(Zero Width Joinner)などを挟んだ文字の場合に、数え方が修正されました。

mb_strwidth("👨‍👩‍👦", "UTF-8");

半角文字を1、全角文字を2とする関数ですが、PHP 8.0以前では5とカウントされるのに、PHP 8.1以降では8とカウントされます。(3v4l: https://3v4l.org/g7eg6)これは、Unicode 11.0からUnicode 13.0への変換テーブルのアップデートが行われたことによるもののようです。mb_strwidthのほか、mb_strimwidth関数にも影響があります。

mb_strimwidth("👨‍👩‍👦‍👦", 1, 2);

https://3v4l.org/gJTeeによると、やはり切り出され方が違っています。このように、半角と全角の概念を使ったmbstring関数の使用に注意してください。(そもそも、半角・全角という概念を用いた数え方が妥当なのかどうかが疑問に感じますが:例えば「○」は1とカウントされますが、日本人ならばおかしいと感じるでしょう)

SJIS-2004(Shift_JIS-2004)のエンコーディングが治っている

これはext/mbstring/tests/sjis2004_encoding.phptを、PHP 8.0に移植してテストしたときにおこった内容です。とある特定の文字を文字のエンコーディングのチェック(mb_check_encodingなど)をかけると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合はこちら

var_dump(
    mb_convert_encoding(
        $bin = hex2bin("82ad9951"), // く儔
        "UTF-8", 
        "SJIS-2004"
    ),
    mb_check_encoding($bin, "SJIS-2004")
);

82adも9951もSJIS-2004では正しい文字なのにも関わらず、PHP 8.0では何故かこの2つの文字を組み合わせるとmb_check_encodingでfalseを返していました。それがPHP 8.1で治っていて、trueを返すようになります。

SJIS-mac(MacJapanese)のエンコーディングが治っている

これもext/mbstring/tests/sjismac_encoding.phptを、PHP 8.0に移植してテストしたときに起こった内容です。とある特定の文字をmb_check_encodingなどでかけると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合はこちら

var_dump(
    mb_convert_encoding(
        $bin = hex2bin("8bcb9b46e6fc9bac82998f818aca816287b38aa9"), // 桐妲蹊岻y潤缶|㌦勧
        "UTF-8", 
        "SJIS-mac"
    ),
    mb_check_encoding($bin, "SJIS-mac")
);

hex2bin("8bcb9b46e6fc9bac82998f818aca816287b38aa9")はSJIS-2004では正しい文字なのにも関わらず、PHP 8.0では何故かこの2つの文字を組み合わせるとmb_check_encodingでfalseを返していました。それがPHP 8.1で治っていて、trueを返すようになります。

SJIS-macにて特殊なローマ数字を当てると不正なエンコーディングとなる

これはちょっとまだよくわかっていないのですが、SJIS-macにて以下のコードでPHP 8.0とPHP 8.1とで違う挙動をします。

var_dump(
    mb_convert_encoding(
        $bin = hex2bin("f8620058004900490049"),
        "UTF-8", 
        "SJIS-mac"
    ),
    mb_check_encoding($bin, "SJIS-mac")
);

ローマ数字である0x85abはSJIS-macでは13を示すXIIIなのです。これを0xf8620058004900490049としてマッピングすることができるということのようなのですが、トランスエンコーディングの処理が間違っていたようだったために起こった変更のようです。(ごめんなさい、MacJapaneseがよくわかってないのでわかってない)

出典: Major overhaul of mbstringのUnicode -> SJIS-mac conversion doesn't reject valid codepoints after a bad transcoding hint

0x5c(バックスラッシュ)をUTF-8などにエンコードすると、0xc2a5(円記号)に変換される

おそらく、バックスラッシュを円記号として解釈するということかと思います。これは正しい変換方法ですが、後方互換性が破壊されているので気をつけてください。(これはIssue行きか?)

例えば、以下のようなコードになります。3v4lで確認したい場合にはこちら

var_dump(
    mb_convert_encoding(
        $bin = hex2bin("5c") . "\\",
        "UTF-8", 
        "SJIS"
    ),
    mb_check_encoding($bin, "SJIS"),
    bin2hex($bin)
);

UTF-32のBOMが削除される

これは主にUTF-16やUCS-4への一貫性への対処とあります。出典: Leading BOM is stripped for UTF-32

var_dump(
    mb_convert_encoding(
        $bin = "\x00\x00\xfe\xff\x00\x11\x00\x00",
        "UCS-4BE", 
        "UTF-32"
    ),
    mb_check_encoding($bin, "UCS-4BE")
);

UCS-2のBOMが削除される

これは主にUTF-16、UTF-32、UCS-4への一貫性への対処とあります。出典: Enhance mbstring support for UCS-2 text

var_dump(
    mb_convert_encoding(
        $bin = "\x00\x00\xfe\xff\x00\x11\x00\x00",
        "UCS-2", 
        "UTF-32"
    ),
    mb_check_encoding($bin, "UCS-2")
);

CP932の文字エンコーディングの対応強化

元のコミットがEnhance handling of CP932 text encodingとなっているので、対応強化と書きましたが、CP932の文字エンコーディングの処理が治っています。

また、制御文字が混ざることを許可しないようにもなったようです(コミットメッセージのコメントより)

var_dump(
    mb_convert_encoding(
        $bin = hex2bin("ed40"),
        "UTF-8", 
        "CP932"
    ),
    mb_check_encoding($bin, "CP932")
);

3v4lで確認したい場合にはhttps://3v4l.org/nS7vGへ。

SJIS-Mobile (DoCoMo、KDDI、Softbank)のUnicode <-> SJIS-Mobileマッピングの変更

出典: Fix mbstring support for SJIS-Mobile (DoCoMo, KDDI, and Softbank variants of Shift-JIS)

SJIS-MobileのUnicodeとのマッピングが変更されているようです。以下のコードから違いがわかるかと思います。

var_dump(
    mb_convert_encoding(
        $bin = hex2bin("9dc8f1d78bc795afe3e0f844e0c0eef08d4df349"),
        "UTF-8", 
        "SJIS-Mobile#DOCOMO"
    ),
    mb_check_encoding($bin, "SJIS-Mobile#DOCOMO")
);

3v4lhttps://3v4l.org/uJKcUを見ると分かる通り、UnicodeとSJIS-Mobileとのマッピングが変わっています。SJIS-MobileからUnicodeへ変換するときに、違う文字に変換されている可能性があります。

CP51932(EUC-JPのマイクロソフトのWindows-31Jの互換表現)のエンコーディングが治っている

ext/mbstring/tests/cp51932_encoding.phptをPHP 8.0に移植してテストをしたときに起こった内容です。CP51932にて、とある特定の文字をmb_check_encodingにかけると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合にはこちら

var_dump(
    mb_convert_encoding(
        $bin = hex2bin("d0d1ecb5dac2adfb4cd4ede7f4"),
        "UTF-8", 
        "CP51932"
    ),
    mb_check_encoding($bin, "CP51932")
);

hex2bin("d0d1ecb5dac2adfb4cd4ede7f4")はCP51932では正しい文字なのにも関わらず、PHP 8.0ではなぜかmb_check_encodingではfalseを返していました。PHP 8.1では治っていて、trueを返すようになります。

CP50220(ISO-2022-JPのマイクロソフトのWindows-31Jの互換表現)のエンコーディングが治っている

ext/mbstring/tests/cp5022x_encoding.phptをPHP 8.0に移植してテストしたときに起こった内容です。CP50220にて、とある特定の文字をmb_check_encodingにかけると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合にはこちら

var_dump(
    $conv = mb_convert_encoding(
        $bin = hex2bin("1b284200"),
        "UTF-8", 
        "CP50220"
    ),
    mb_check_encoding($bin, "CP50220"),
    bin2hex(mb_convert_encoding($conv, "CP50220", "UTF-8"))
);

UTF7-IMAPのエンコーディングが治っている

ext/mbstring/tests/utf7imap_encoding.phptをPHP 8.0に移植してテストしたときに起こった内容です。UTF7-IMAPにて、とある特定の文字をmb_check_encodingにかけると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合にはこちら

var_dump(
    $conv = mb_convert_encoding(
        $bin = hex2bin("00"),
        "UTF-8", 
        "UTF7-IMAP"
    ),
    mb_check_encoding($bin, "UTF7-IMAP"),
    bin2hex(mb_convert_encoding($conv, "UTF7-IMAP", "UTF-8"))
);

これは今までと違い、不正な文字列を不正な文字とチェックすることができるようになったということでしょうか。

ISO-2022-JP-KDDIのエンコーディングが治っている

ext/mbstring/tests/iso2022jp_kddi_encoding.phptをPHP 8.0に移植してテストしたときに起こった内容です。ISO-2022-JP-KDDIにて、とある特定の文字をmb_check_encodingにかけると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合にはこちら

var_dump(
    $conv = mb_convert_encoding(
        $bin = hex2bin("1b284200"),
        "UTF-8", 
        "ISO-2022-JP-KDDI"
    ),
    mb_check_encoding($bin, "ISO-2022-JP-KDDI"),
    bin2hex(mb_convert_encoding($conv, "ISO-2022-JP-KDDI", "UTF-8"))
);

ISO-2022-JP-MSのエンコーディングが治っている

ext/mbstring/tests/iso2022jp_ms_encoding.phptをPHP 8.0に移植してテストしたときに起こった内容です。ISO-2022-JP-KDDIにて、とある特定の文字をmb_check_encodingにかけると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合にはこちら

var_dump(
    $conv = mb_convert_encoding(
        $bin = hex2bin("1b284200"),
        "UTF-8", 
        "ISO-2022-JP-MS"
    ),
    mb_check_encoding($bin, "ISO-2022-JP-MS"),
    bin2hex(mb_convert_encoding($conv, "ISO-2022-JP-MS", "UTF-8"))
);

UTF-16からUTF-32へのエンコーディングが変化している

ext/mbstring/tests/utf_encodings.phptをPHP 8.0に移植してテストしたときに起こった内容です。何故かはまだわかっていませんが、UTF-16からUTF-32へmb_convert_encodingすると、内容が変化します。

例えば、以下のようなコードになります。3v4lで確認したい場合にはこちら

var_dump(
    $conv = mb_convert_encoding(
        $bin = hex2bin("dc01d8020042"),
        "UTF-32", 
        "UTF-16"
    ),
    mb_check_encoding($bin, "UTF-32"),
    bin2hex(mb_convert_encoding($conv, "UTF-16", "UTF-32"))
);

UTF-8はパスできたのですが、UTF-16はパスできなかったため、UTF-7とUTF-32のエンコーディングが変化している可能性があります。ただし、いずれも不正な文字での検証になっていると思われます。

SJIS-openがCP932にまとめられた

SJIS-openとは、suikawikiによると、UI-OSF 日本語環境実装規約版シフトJISとありますが、それがCP932にまとめられました。

出典: Remove duplicate implementation of CP932 from mbstring

ただ、「95-104区は EUC の JIS X 0208 の85-94区に対応付けられていました。」「105-114区は EUC の JIS X 0212 の95-94区に対応付けられていました。」「115-120区は IBM 拡張文字でした。」の部分がちょっと謎に思いました。

OSF 日本ベンダ協議会 (OSF/JVC) 推奨 日本語 EUC ・シフト JIS 間コード変換仕様とコード系実態調査(Internet Archiveより)

まだ調査中です

まだ良くわかっていないMajor overhaul of mbstringですが、この問題に対してどういう変更が入っているのかというのを理解していく必要があります。もしみつけたらphp-srcへIssueを投げてもらえると皆が助かります。ドキュメントに問題があったらドキュメントにIssueを投げてもらえると助かります。