LoginSignup
0
1

More than 1 year has passed since last update.

BufferとTextDecoderの微妙な差

Last updated at Posted at 2022-08-15

Node.jsの標準で存在するBufferと、WHATWG発祥のTextDecoderでそれぞれバイト列をUTF-8として解釈できますが、微妙に結果が違うことに気づきました。

TL; DR

  • 正当なUTF-8となるバイト列であれば、BufferTextDecoderの結果に差は出ない
  • 途切れたマルチバイト文字の解釈で、生成される(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つを生成する」と一貫しているのも悪くないなと思いました。

脚注

  1. 0xC0や0xC1もこの範囲には入りますが、後述するようにこれらのバイトから始まる文字は「冗長なエンコード」となって、正当なUTF-8とはなりません。

  2. 0xF5 - 0xF7もこのビットパターンですが、後述のようにUnicodeの範囲からはみ出すので、正当なUTF-8で使われることはありません。

  3. 「CESU-8」と称してこのような、UTF-8でサロゲートペアを使うエンコードがなされる例もあります。

0
1
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
0
1