正規化

符号化方式と正規化の問題を激しく混同した解説をどうも。ブックマークコメントをみても正しく問題が伝わっていないように思える。というか、書いた人がきちんと認識してないんじゃないか。

2007年09月04日 omaya omaya 誰が悪いんだろう。

強いて言えば NFD な Unicode の入力に対してまともに動かない Web アプリじゃないかな。

2007年09月04日 mattn mattn macosx, unicode ブラウザのバグだしバージョンで処理しないといけないのかな...

ブラウザのバグではない。

しかもややこしいことに、UTF-8で濁点をあらわすコードは「U+309B」(KATAKANA-HIRAGANA VOICED SOUND MARK)で、 UTF-8-MACから送信される「U+3099」(COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK)とは別物です。

ややこしくないよ。正規化を理解していれば当たり前のことだよ。
というか、これ、普通に NFD な入力をきちんと処理できるようにするだけでいいと思うんだけど。もしかして、PostgreSQL は NFD な Unicode を格納するとだめだったりする?さらに言えば、U+0451(ё)が入った場合とかどうするの?合成文字って濁音/半濁音だけじゃないし、ファイルアップロード時だけじゃなく、Finder からファイル名を直接コピーしたりしても NFD な Unicode が送信される可能性がある。
とりあえず、検証用の適当スクリプト

<?php
mb_internal_encoding('UTF-8');
header('Content-type: text/html; charset=UTF-8');

function dump()
{
    $texts = array();
    if (array_key_exists('text', $_POST)) {
        $texts[] = $_POST['text'];
    }
    if (array_key_exists('f', $_FILES)) {
        $texts[] = $_FILES['f']['name'];
    }

    foreach ($texts as $text) {
        $l = mb_strlen($text);
        for ($i = 0; $i < $l; $i++) {
            $s = mb_substr($text, $i, 1);
            $u = mb_convert_encoding($s, 'UCS-4BE');
            $u = unpack('N', $u);
            echo htmlspecialchars(sprintf('%s U+%04x', $s, $u[1]));
            echo "\n";
        }
        echo "\n";
    }
}

$t = array_key_exists('text', $_GET) ? htmlspecialchars($_GET['text']) : '&#12495;&#12441';
?>
<html>
  <body>
    <form action="<?= htmlspecialchars($_SERVER['PHP_SELF']) ?>" method="POST" enctype="multipart/form-data">
      <input type="text" name="text" value="<?= $t ?>"/>
      <input type="file" name="f"/>
      <input type="submit"/>
    </form>
    <pre><? dump() ?></pre>
  </body>
</html>

追記

ICU を使った PHP 用正規化ライブラリ

追記 2

PHPのmbstringの設定で、 mbstring.http_inputとmbstring.internal_encodingの値が異なり、 mbstring.encoding_translationの値がonになっている場合、 サーバ側で入力パラメータのエンコーディングが自動変換されます。 その場合、NFDで正規化された合成文字は正しく変換されずに 途中で切れてしまう場合があります (そもそもPHPのmbstringモジュールがNFDに対応していないため)。

あぁ、内部は EUC-JP なんかの UTF-8 以外のエンコーディングで処理しているわけか。手元の PHP 4.4.4 で UTF-8 から EUC-JP に変換したら濁点部分(U+3099)が「?」になるけど、別にそれ以降が切れたりすることは無い。そのへんはバージョン依存なのかな。

追記 3

何のことを言っているか分からない人のために。
Unicode では、濁音/半濁音/アクセント記号(ウムラウト等)がついた文字を合成文字1文字(ガ(U+30AC))で表すことも、基底文字 + 結合文字(カ(U+30AB) + ゛(U+3099))で表すこともできる。前者の方法で正規化したものを NFC(Normalization Form Composition)、後者で正規化したものを NFD(Normalization Form Decomposition)という。他にも NFKC と NKFD があるが略。これは文字の表現形式の話で、エンコーディングとは違うレイヤの問題なんだけども、非 Unicodeエンコーディングには結合文字というのがないのが普通なので、NFD のままだとエンコーディングの変換に失敗したりする。
でまぁ、普通に入力するとたいてい NFC になっているので問題にならないが、Mac OS Xファイルシステムはファイル名を NFD で格納するので、Mac OS X のファイルを扱うと問題になったりするわけだ。Mac OS X も普通に入力したテキスト自体は NFC になっている。