LoginSignup
45
39

More than 5 years have passed since last update.

PHP 7.2 の開発版で導入された mb_chr、mb_ord、mb_scrub を試す

Last updated at Posted at 2016-09-05

概要

PHP 7.2 の開発版 (2016年9月時点で master ブランチ)に mb_chrmb_ordmb_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_chrmb_ordmb_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::chrIntlChar::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
test.php
var_dump(
    'a' === mb_chr(0x61),
    0x61 === mb_ord('a')
);

PHP 5.6 以降の php.ini のディレクティブ

仕様変更の経緯については yohgaki さんが策定したRFC をご参照ください。

PHP 5.6 以降 mbstring.internal_encodingmbstring.http_inputmbstring.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_encodinginput_encodingoutput_encoding ディレクティブが追加されました。これらのデフォルト値が空の場合、default_charset の値が使われます。

RFC によれば、htmlentitiesmb_strlenmb_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-JPBASE64 のような通信や変換の用途に限定される文字エンコーディングが指定された場合、警告が発せられ、戻り値は 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())
);

標準関数の htmlspecialcharshtmlspecialchars_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

不正なバイト列が指定された場合、nullfalse を返す選択肢があります。intl の graphme_strlenu オプションを指定した preg_matchjson_encode の戻り値はそのような仕様になっています。このような仕様の問題はユーザーが関数に入力する前にあらかじめ不正なバイト列の対応をしておくことが要求されてしまうことです。不正なバイト列を代替文字に置き換える処理には mb_convert_encoding、後述の mb_scrubUConverter::transcodeUConverter::convert を使います。しかしながら、文字エンコーディングの知識がなければ、不正なバイト列とは何かということがわからなければ、falsenull を返されても何をすればよいのかわからないでしょう。

CMS やフレームワークでこの関数を使う場合、どこで不正なバイト列を代替文字に変換する処理をどこでやるのかを頭に入れる必要があります。

以上のことから、不正なバイト列に遭遇したとき nullfalse を返すもしくは 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 公式マニュアルの追加
  • 標準関数の chrord に文字エンコーディングオプションを追加する提案をする
  • mb_scrub に相当する str_scrub の提案  
45
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
39