Subscribed unsubscribe Subscribe Subscribe

枕を欹てて聴く

香炉峰の雪は簾を撥げて看る

decodeURIComponentのもろもろについて

Unicode ECMAScript

追記1
SpiderMonkeyサロゲートエリアのbug issue は修正されました! (該当commit) 記事の一番下を御覧ください.



追記2
V8のサロゲートエリアのbug issue は修正されました!(該当commit)

これで以下の記事のサロゲートエリアのbugはV8, SpiderMonkey, JSCで修正されました



普段1.5ヶ月に1記事しか書かないのに, 今日は3つも書いて正気の沙汰じゃないで...
id:piro_or さんよりcommentで, id:nanto_vi さんの以下の記事の話を受けましたのでー. (commentついたの1年ぶりで感涙)
文字列と UTF-8 バイト列の相互変換: Days on the Moon
リンク先の記事では, 以下の方法によるUTF-16 <=> UTF-8の変換が記載されています.

function toUTF8Octets(string) {
  return unescape(encodeURIComponent(string));
}

function fromUTF8Octets(octets) {
  return decodeURIComponent(escape(octets));
}

素晴らしいことに, これで要件を済ませることができます. すごい!! さっすがnanto_viさん! engineが正確に仕様準拠であるならば, 完全にこれで大丈夫です. lv5でも確認できました.
なぜなら, ES5.1, section 15.1.3にあるとおり, encodeURIComponentはUTF-16のmulti byte文字列を%HH%HHという風にUTF-8にconvertし, unescapeはAnnex B2.2をみると, %xy形式のものをHexDigitとしてcode pointを構築し, 何のcheckもなくそのままUTF-16 code pointとしてappendしてしまうからです(step 14). 結果返ってくるstringにはcode pointとして生のUTF-8と全く同じものが入ります. 逆も同様にです. 素晴らしいことに!!!

ただし... edge case

ただし現実のengineはES5仕様の楽園に住んでいるわけではなく, 完全に仕様準拠というわけでもないので, 2011/05/30現在, edge caseに限りおかしな変換結果を返します. これは2つpatternがあるのですが, まずは将来的にも治らないであろう, 意図的な仕様違反を見てみます.

decodeURIComponent(encodeURIComponent('\uFFFF')) !== '\uFFFF'

SpiderMonkeyでは,

decodeURIComponent(encodeURIComponent('\uFFFE')) !== '\uFFFE';
decodeURIComponent(encodeURIComponent('\uFFFF')) !== '\uFFFF';

です. ES5仕様に照らし合わせれば仕様違反ではあるのですが, 意図的なものです.
これはencodeURIComponentまでは正確なのですが, decodeURIComponentでUTF-16に直す時に意図的に置き換えるためです.








https://bugzilla.mozilla.org/show_bug.cgi?id=520095#c2
というわけで意図的に, BOMと誤認識するかもしれない\uFFFE, \uFFFFを\uFFFDに変換します.
よって, \uFFFD, \uFFFFを含む文字列の場合, SpiderMonkeyでは上記の方法は失敗します.

fromUTF8Octets(toUTF8Octets('\uFFFF')) === '\uFFFD';

となってしまうためです. edge case...
ただ, 上記でも触れられているとおり, \uFFFE, \uFFFFに文字が与えられる場合はないので, 通常の文字列の場合は問題ないでしょう. ただ, その特性を利用して文字の区切りとかに使っていたり, UTF-8がこのような表現がたまたま入っていた場合は, \uFFFDという普通の文字に変えられてしまい, \uFFFFの文字として現れないという特性が失われてしまったりするという感じです.

サロゲートエリアさん...

このUnicode変換を実装し, RFC3629を読んでいて, なるほどここに気をつけないといけないのかーと思いながら書いていたのですが, ふと見るとある気をつけるべき部分が「あれっ? この対策ってSpiderMonkeyやってたcodeを見た覚えがない...」となっていた部分でもあります.
それは, 3bytesのUTF-8 => UCS4変換におけるサロゲートエリアの除外です. 上記記事でも一度変換を弾くように書いている,

%xED %A0 10000000

すなわち, encodeURIComponent表現で%ED%A0%80をSpiderMonkeyのdecodeURIComponentに食べさせてみましょう.

decodeURIComponent('%ED%A0%80') === '\uD800'

というわけでURIError飛ばずに通ってしまいました. 果たしてこれは仕様と照らし合わせてどうなのか.
みんな大好きECMA262 5.1th section 15.1.3 Decodeのstep 4-d-vii-8によると,

Let V be the value obtained by applying the UTF-8 transformation to Octets, that is, from an array of octets into a 32-bit value. If Octets does not contain a valid UTF-8 encoding of a Unicode code point throw a URIError exception.

http://es5.github.com/#x15.1.3

というわけで, RFC3629に照らし合わせればこれはvalid UTF-8 encodingではないので, URIErrorが出されねばならず, 仕様違反であると解釈できます.

ということは, このようなbyte列,

var str = "\u00ED\u00A0\u0080";
fromUTF8Octets(str);  // this is not valid UTF-8, so must throw error

invalidにもかかわらず, "\uD800"が出てしまいます. これならまったくサロゲートペア関係ないinvalidなUTF-8 3bytesのcode point2つつかって, サロゲートペアと誤認識させることができたりしますね...

というわけで, ぶっちゃけ2つめはengineのbugです. V8, SpiderMonkeyで確認しました. JSCにはこのbugがありませんでした. wtf/unicode/UTF.cppのdecodeUTF8Sequence関数のHandle 3bytes sequencesをみるときちんと,

    // Handle 3-byte sequences.
    if ((b2 & 0xC0) != 0x80)
        return -1;
    const unsigned char b3 = sequence[3];
    if (length == 3) {
        if (b3)
            return -1;
        const int c = ((b0 & 0xF) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F);
        if (c < 0x800)
            return -1;
        // UTF-16 surrogates should never appear in UTF-8 data.
        if (c >= 0xD800 && c <= 0xDFFF)
            return -1;
        return c;
    }

surrogate areaは弾くようにきちんと対策されていますね. lv5は下のentryに書いたC++のcodeを用いているのでもちろん弾きます.

結論

というわけで, SpiderMonkey, V8にそれぞれissueをreportしました.
http://code.google.com/p/v8/issues/detail?id=1415
https://bugzilla.mozilla.org/show_bug.cgi?id=660612
lv5開発, 変なところで貢献してるな...

非常に面白いお話をありがとうございました.

追記
id:koichik さんよりcommentを頂きました. ありがとうございますー. ということで, V8はUTF-16でなくUCS-2をサポートしている, つまりsurrogate pairはそもそもサポートしていないそうです, ってまじですか...

追記
fixされました.
http://hg.mozilla.org/mozilla-central/rev/0d5626387b8a