概要
PHP 7.2 の開発版 (2016年9月時点で master ブランチ)に mb_chr
、mb_ord
、mb_scrub
が導入されました。これらの関数は私 (masakielastic) が実装を提案し、yohgaki さんが検証と導入をしました。
PHP のソースコードの NEWS ファイルには次のような記載があります。
- Mbstring
- Implemented request #66024 (mb_chr() and mb_ord()) (Masakielastic, Yasuo)
- Implemented request #65081 (mb_scrub()) (Masakielastic, Yasuo)
polyfill-mbstring を利用する
mbstring がインストールされていない環境もしくは PHP 7.2 以前のバージョンで mb_chr
、mb_ord
、mb_scrub
を採用したい場合、Symfony の polyfill-mbstring コンポーネントを導入する選択肢があります。Composer による
composer require symfony/polyfill-mbstring
ただし、この記事で書かれている機能がすべて再現されているとはかぎりません。
開発版をビルドする
最新の履歴をだけをもつ shallow clone をつくります。
git clone --depth=1 git@github.com:php/php-src.git
cd php-src
./buildconf
./configure --enable-mbstring
make
mbstring モジュールが読み込まれているか確認しましょう。
sapi/cli/php -m | grep mbstring
intl モジュールを有効にする
PHP 7.0 で導入された IntlChar::chr
、IntlChar::ord
を比較のために使いたい場合、intl モジュールを有効にするビルドオプションを追加します。
./configure --enable-mbstring --enable-intl
shallow を解除してリポジトリを更新する
git fetch
もしくは git pull
を実行する際に --unshallow
オプションを指定して shallow を解除します。shallow を解除しないと merge
されません。
git pull --unshallow
関数が利用できるか確認する
次のコマンドを実行して小文字の「a」が表示されるか試してみましょう。
sapi/cli/php -r 'echo mb_chr(0x61), PHP_EOL;'
今度はスクリプトファイルを用意して実行してみましょう。
sapi/cli/php test.php
var_dump(
'a' === mb_chr(0x61),
0x61 === mb_ord('a')
);
PHP 5.6 以降の php.ini のディレクティブ
仕様変更の経緯については yohgaki さんが策定したRFC をご参照ください。
PHP 5.6 以降 mbstring.internal_encoding
、mbstring.http_input
、mbstring.http_output
は非推奨になりました。これらのディレクティブの値を設定しない場合、default_charset
の値が使われます。default_charset
のデフォルト値は UTF-8
です。
var_dump(
'UTF-8' === ini_get('default_charset')
);
PHP 5.6 以降では htmlspecialchars
の文字エンコーディングのデフォルト値に default_charset
が使われます。PHP 5.4、5.5 では htmlspecialchars
の文字エンコーディングのデフォルト値が UTF-8
でした。
同時に internal_encoding
、input_encoding
、output_encoding
ディレクティブが追加されました。これらのデフォルト値が空の場合、default_charset
の値が使われます。
RFC によれば、htmlentities
、mb_strlen
、mb_regex
などの関数のデフォルト値に internal_encoding
が使われるとのことですが、2016年9月時点の調査では ini_set
でディレクティブの値を変更しても mb_internal_encoding
の戻り値には反映されませんでした。
ini_set('internal_encoding', 'Shift_JIS');
var_dump(
'UTF-8' === mb_internal_encoding()
);
関数の仕様
mb_chr
string mb_chr(int $codepoint[, string $encoding = mb_internal_encoding()])
mb_chr
はコードポイントを文字に変換する関数です。関数のパラメータは $codepoint
と $encoding
です。$codepoint
はコードポイントです。コードポイントは1文字をあらわす整数の値です。コードポイントの値は UTF-8 や Shift_JIS などの文字エンコーディングごとに異なります。$encoding
は省略できます。
関数の戻り値はコードポイントに対応する文字です。
最初に UTF-8 を試してみましょう。
var_dump(
"あ" === mb_chr(0x3042)
);
日本語フォントや絵文字がインストールされていない端末でスクリプトコードを開くことを想定する場合、PHP 7.0 で導入された Unicode エスケープシーケンスを使うことができます。
var_dump(
"\u{3042}" === mb_chr(0x3042)
);
Shift_JIS の場合、文字を構成するバイト列をそのまま16進数であらわしたものです。
$char = mb_convert_encoding('あ', 'Shift_JIS', 'UTF-8');
var_dump(
$char === mb_chr(0x82a0, 'Shift_JIS'),
"82a0" === bin2hex(mb_chr(0x82a0, 'Shift_JIS')),
"\x82\xa0" === mb_chr(0x82a0, 'Shift_JIS')
);
レガシーエンコーディングのバイト数の上限は4バイトです。もしそれ以上のバイト数がある事例があったら教えてください。中国語の文字エンコーディングである GB18030 で絵文字を試してみましょう。私は中国語のフォントがインストールされた端末をもっていないので、どのように表示されるのかは確認していません。
// U+1F418: Elephant
$char = mb_convert_encoding("\u{1F418}", 'GB18030', 'UTF-8');
var_dump(
$char === mb_chr(0x9439cb38, 'GB18030'),
"\x94\x39\xCB\x38" === mb_chr(0x9439cb38, 'GB18030')
);
今度はUTF-8 の無効なコードポイントを試してみましょう。戻り値は mbstring.substitute_character
の値に対応する文字です。デフォルト値は U+003F です。
var_dump(
'?' === mb_chr(-1),
"\u{003f}" === mb_chr(-1)
);
UTF-8 では代替文字として U+FFFD が定義されています。
mb_substitute_character(0xfffd);
var_dump(
"\u{fffd}" === mb_chr(-1)
);
ISO-2022-JP
や BASE64
のような通信や変換の用途に限定される文字エンコーディングが指定された場合、警告が発せられ、戻り値は false
になります。
var_dump(false === mb_chr(0x61, 'ISO-2022-JP'));
mb_ord
int mb_ord(string $char[, string $encoding = mb_internal_encoding()])
mb_ord
は文字をコードポイントに変換する関数です。mb_ord
のパラメータは $char
と $encoding
です。$char
は文字で、$encoding
は省略できます。戻り値は $char
に対応するコードポイントです。
まずは UTF-8 を試してみましょう。
var_dump(
0x3042 === mb_ord('あ'),
0x3042 === mb_ord("\u3042")
);
不正なバイト列が指定された場合、mbstring.substitute_character
の値が返されます。
var_dump(
0x3f === mb_ord("\x80")
);
代替文字の U+FFFD を試してみましょう。
mb_substitute_character(0xfffd);
var_dump(
0xfffd === mb_ord("\x80")
);
mb_scrub
string mb_scrub(string $str[, string $encoding = mb_internal_encoding()])
mb_scrub
は文字列に含まれる不正なバイト列を代替文字に置き換える関数です。scrub の意味は「ゴシゴシと汚れを落とす、ゴシゴシと洗う」と言った意味です。Ruby 2.1.0 で導入された String#scrub
メソッドの名前から採用しました。
mb_scrub
のパラメータは $str
と $encoding
です。$encoding
は省略できます。戻り値はパラメータの文字列に含まれる不正なバイト列を代替文字に置き換えたものです。代替文字には mbstring.substitute_character
の値が使われます。
var_dump(
"あ?" === mb_scrub("あ\x80")
);
別の代替文字を試してみましょう。
mb_substitute_character(0xfffd);
var_dump(
"あ\u{fffd}" === mb_scrub("あ\x80")
);
mb_scrub
の代わりに mb_convert_encoding
を使って不正なバイト列を置き換えることもできます。
var_dump(
"あ?" === mb_scrub("あ\x80"),
"あ?" === mb_convert_encoding("あ\x80", mb_internal_encoding())
);
標準関数の htmlspecialchars
と htmlspecialchars_decode
を組み合わせることで同じ処理ができます。
var_dump(
"あ&\u{fffd}" === htmlspecialchars_decode(htmlspecialchars("あ&\x80", ENT_SUBSTITUTE))
);
intl モジュールが導入されていれば UConverter
を使うことができます。このクラスは PHP 5.5 の時代に導入されました。
var_dump(
"あ\u{fffd}" === UConverter::transcode("あ\x80", 'UTF-8', 'UTF-8'),
"あ\u{fffd}" === (new UConverter('UTF-8', 'UTF-8'))->convert("あ\x80")
);
開発者ノート
UTF-8 の基礎知識
UTF-8 の有効なコードポイントは U+0000 から U+D7FF、U+E000 から U+10FFFF までの範囲です。U+D800 から U+DFFF の範囲はサロゲート文字と呼びます。UTF-16 では U+FFFF までのコードポイントしか使えないので、U+10000 から U+10FFFF までの範囲の文字をあらわすのにサロゲートペア (2つのサロゲート文字) を使います。
自分で UTF-8 の処理を書く場合、次の記事をご参照ください。
mb_chr
無効なコードポイントが引数として指定された場合、関数のふるまいとして fatal error を発して処理を停止させる選択肢があります。この選択肢の場合、関数のユーザーは処理を停止させないために有効なコードポイントを知っている必要があります。世界にはさまざまな文字エンコーディングが存在するので、CMS やフレームワークの開発者に過大な知識が要求されてしまいます。
このことから標準関数を実装する場合、「その関数を使うための最小限の知識とは何か?」を問う必要があることが言えるでしょう。
mb_chr
の引数に関して、ループを使わずに複数のコードポイントから文字列を生成する利便性を考えるなら、第1引数が配列を受け取るもしくは JavaScript の fromCodePoint
のように可変引数を選ぶことが考えられます。コードとデータを分離したり、要素数の多い配列を扱うことを考えると、配列を受け取る方式のほうが使いやすいのではないでしょうか。
mb_ord
不正なバイト列が指定された場合、null
や false
を返す選択肢があります。intl の graphme_strlen
や u
オプションを指定した preg_match
、json_encode
の戻り値はそのような仕様になっています。このような仕様の問題はユーザーが関数に入力する前にあらかじめ不正なバイト列の対応をしておくことが要求されてしまうことです。不正なバイト列を代替文字に置き換える処理には mb_convert_encoding
、後述の mb_scrub
、 UConverter::transcode
、UConverter::convert
を使います。しかしながら、文字エンコーディングの知識がなければ、不正なバイト列とは何かということがわからなければ、false
や null
を返されても何をすればよいのかわからないでしょう。
CMS やフレームワークでこの関数を使う場合、どこで不正なバイト列を代替文字に変換する処理をどこでやるのかを頭に入れる必要があります。
以上のことから、不正なバイト列に遭遇したとき null
や false
を返すもしくは fatal error にする仕様は選びませんでした。
関数のふるまいについて参考にしたのは Go です。Go では for 文と range
を組み合わせて、文字列から1文字ずつ取り出す場合、不正なバイト列を U+FFFD に変換します。Go のふるまいの調査についてはこちらの記事をご参照ください。Go の作者であるロブ・パイクは UTF-8 の開発にも携わりました。
mb_scrub
私が知るかぎり、PHP 5.2 の時代から不正なバイト列を置き換えるために mb_convert_encoding
が使われていましたが、変換前と変換後の文字エンコーディングが同じであるために、はじめてコードを見た人には何をやっているのかまったくわからないでしょう。関数の名前を提供されることで、どんな処理をやっているのか伝わりやすくなります。
前述のとおり、mb_scrub の名前は Ruby の String#scrub
から採用しました。開発の経緯についてはこちらの記事をご参照ください。ユースケースは文字列の切り詰めや部分文字列の取り出し操作によって壊れてしまった文字が含まれているときの対応です。
不正なバイト列に関連するセキュリティ問題は 徳丸先生 (ockeghem) の「文字コードの脆弱性はこの3年間でどの程度対策されたか?」(2014年) のスライドや「体系的に学ぶ 安全なWebアプリケーションの作り方」(2011年、SBクリエイティブ) をご参照ください。
今後の課題
- PHP 公式マニュアルの追加
- 標準関数の
chr
、ord
に文字エンコーディングオプションを追加する提案をする -
mb_scrub
に相当するstr_scrub
の提案