Node.jsの標準で存在するBuffer
と、WHATWG発祥のTextDecoder
でそれぞれバイト列をUTF-8として解釈できますが、微妙に結果が違うことに気づきました。
TL; DR
- 正当なUTF-8となるバイト列であれば、
Buffer
とTextDecoder
の結果に差は出ない - 途切れたマルチバイト文字の解釈で、生成される
�
(U+FFFD)の数が違う - この挙動差に依存するコード自体に嫌な雰囲気を感じる一方、
Buffer
の挙動のほうが一貫している印象を受けた
気づいたきっかけ
Buffer
で書かれたコードをTextDecoder
に直したところ、テストがコケてしまいました。テストの状況を確認してみたところ、正常系では特に問題がなかったのですが、正しくUTF-8として解釈できないコードで、�
(U+FFFD)の個数が違っていました。
UTF-8の構造
正しくないコードについて語るために、ここでUTF-8の構造について整理しておきます。日本語用のシフトJISやEUCでは、1バイト目と2バイト目で同じバイトを使っているために、最悪の場合先頭から読まないと文字の区切りが把握できない、という事態となってしまいます。これに対して、UTF-8では「1バイト目」と「2バイト目以降」で使うビットパターンを区別することで、最悪の場合でも次の文字に行けば同期できるようになっています。
16進 | ビットパターン | 役割 |
---|---|---|
0x00 - 0x7F | 0xxxxxxx | 1バイト文字 |
0x80 - 0xBF | 10xxxxxx | マルチバイト文字の2バイト目以降 |
0xC21 - 0xDF | 110xxxxx | 2バイト文字の1バイト目 |
0xE0 - 0xEF | 1110xxxx | 3バイト文字の1バイト目 |
0xF0 - 0xF42 | 11110xxx | 4バイト文字の1バイト目 |
なお、0xF8 - 0xFCは、かつてISO 10646が31ビットコードとして定義されていた時代に、5バイト文字や6バイト文字の1バイト目に使われていましたが、のちにISO 10646もUnicodeの範囲まで縮小したため、もはや使われることはありません。また、0xFEと0xFFはBOMの識別用に空けてあります。
不正なコード
上の基本ルールに従わない、たとえば「いきなり2バイト目以降が現れた」なども不正なコードですが、それ以外にも不正なものが存在します。
冗長なエンコード
たとえば0xC0 0x80を機械的に解釈するとU+0000に当たるのですが、これは0x00と書けば済みます。セキュリティ上の理由により、このような「必要以上に多くのバイトでエンコードする」ことは禁止されています。
- 2バイト…0xC0 0x80 - 0xC1 0xBF
- 3バイト…0xE0 0x80 0x80 - 0xE0 0x8F 0xBF
- 4バイト…0xF0 0x80 0x80 0x80 - 0xF0 0x8F 0xBF 0xBF
サロゲートペア
UTF-16で追加面を表すためにサロゲートペア(前半がU+D800 - U+DBFF、後半がU+DC00 - U+DFFF)が導入されていますが、UTF-16以外の符号化方式でサロゲートペアを使うのは禁止されています3。
- 前半…0xED 0xA0 0x80 - 0xED 0xAF 0xBF
- 後半…0xED 0xB0 0x80 - 0xED 0xBF 0xBF
大きすぎる文字
Unicodeの規格上、最後のコードポイントはU+10FFFF(0xF4 0x8F 0xBF 0xBF)です。これ以降のコードポイントは存在しません。
動作検証
実際に規格外のコードを作って動作検証を行ってみました。
See the Pen Untitled by jkr_2255(すんぶ) (@jkr_2255) on CodePen.
ご覧のように、3バイト文字・4バイト文字が2バイト目以降で途切れた場合の結果が違ってきています。
考察
Unicodeの規格書(PDF)では、「不正なコードの処理に正当なコードは巻き込まない」ということは決まっていますが、途切れた文字に対していくつの�
を入れればいいかは規定されていません。
TextDecoderの仕様を確認してみたところ、変換不能な文字は「気づいた箇所の直前」で�
に置き換えるような処理となっていました。各種の不正な文字については2バイト目で判定できるので1バイト目だけ�
として次に進みますが、途切れたパターンでは途切れるまでのシーケンスをまとめて�
にする、という流れでした。
不正なコードの処理の形に依存するコード自体、筋が悪そうな感じしかしないので、どうでもいい部類の差ではあるのですが、Buffer
の「不正なコードポイント1つに対して1つ�
を生成する」と一貫しているのも悪くないなと思いました。