UnicodeとBOM

# UnicodeとBOM

いまや、ごく限られたケースを除いて、新規テキストファイルは思考停止にUTF-8で保存するのがベストプラクティスとなっており、文字エンコーディングにまつわるトラブルは昔ほど日常的でなくなったように思う。
しかし、同じUTF-8でもBOMの有無が問題になることがしばしばあるので、ここにまとめておきたい。

Windowsユーザーであれば、以下のメモ帳の「文字コード」の違いが分かるようになることが、この記事の目標である。

![encodings_in_notepad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413608/b41e11bb-490c-afbf-c3be-ff99ebf5efe2.png)


## バイトオーダー

文字エンコーディングに限らず、データのエンコーディング一般において、1つの値から複数のバイトが得られる場合、それらをどのような順序で並べるかはそれ自体がルール化されるべきことである。

例えば、1,000を16ビット符号無し整数としてエンコードすると、上位バイトが`03`、下位バイトが`e8`となるが、これら2バイトを、

```
03 e8
```

と、上位から並べるか、

```
e8 03
```

と、下位から並べるかは、それ自体がエンコーディングにおける選択肢の一つである。

前者の並べ方を**ビッグエンディアン**（BE）、後者を**リトルエンディアン**（LE）と言う。

TCP/IPなどネットワーク分野ではBEが、ファイルシステムなどそれ以外の場面ではLEが使われることが多い。


## UTFとバイトオーダー

ところで、Unicodeのエンコーディングには、UTF-8, UTF-16, UTF-32などがあり、
それぞれ「🍺(BEER MUG)」を次のように符号化する。[^1]

[^1]: UTF-16を2単位（サロゲートペア）にしたいので、日常的な文字を網羅している基本多言語面(BMP)ではなく、あえて[追加多言語面](https://ja.wikipedia.org/wiki/%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E8%A8%98%E5%8F%B7%E5%8F%8A%E3%81%B3%E7%B5%B5%E8%A8%98%E5%8F%B7)から持ってきた。

| エンコーディング | 符号単位の列         |
| ---------------- | ------------------- |
| UTF-8            | `f0` `9f` `8d` `ba` |
| UTF-16           | `d83c` `df7a`       |
| UTF-32           | `0001f37a`          |

ここで、右列の各ブロックは **符号単位** の16進表記である。

これらのUTF系エンコーディングは、文字（コードポイント）を直接バイト列に結びつけるのではなく、符号単位という整数の列に変換するものとして規定されている。
8, 16, 32という数値が表しているのは、この符号単位1つあたりのビット数である。

```mermaid
graph LR;
S(文字) -- UTF --> E(符号単位の列)
```

すると、UTF-16やUTF-32の場合、個々の符号単位がマルチバイトであるため、前述のバイトオーダーが問題となる。
バイトオーダーの選択によって、それぞれが2つの「文字 → バイト列」変換を生み出すわけである。

```mermaid
graph LR;
S(文字) -- UTF-16 / UTF-32 --> E(符号単位の列) -- バイトオーダー --> B(バイト列)
```

対照的に、符号単位が8ビット = 1バイトであるUTF-8では、バイトオーダーは問題にならない。
符号単位そのものの並べ方は、各UTFそれ自体が規定しているということに注意してほしい。

```mermaid
graph LR;
S(文字) -- UTF-8 --> E(符号単位の列 = バイト列)
```

結果として、最終的に得られるバイト列の候補は次のようになる。

| エンコーディング | バイト列             |
| ---------------- | ------------------- |
| UTF-8            | `f0` `9f` `8d` `ba` |
| UTF-16 + BE      | `d8` `3c` `df` `7a` |
| UTF-16 + LE      | `3c` `d8` `7a` `df` |
| UTF-32 + BE      | `00` `01` `f3` `7a` |
| UTF-32 + LE      | `7a` `f3` `01` `00` |

## BOM

バイトオーダーを区別するために（あるいはそもそもUTFでエンコードされていることを表すために）、一種のメタ情報として、テキストの先頭に **バイトオーダーマーク**（BOM） を付加することがある。

これは、コードポイント`U+FEFF`で表される文字で、UTF-16, UTF-32では、次のバイト列になる。

| エンコーディング    | BOM（バイト列）     |
| ---------------- | ------------------- |
| UTF-16 + BE      | `fe` `ff`           |
| UTF-16 + LE      | `ff` `fe`           |
| UTF-32 + BE      | `00` `00` `fe` `ff` |
| UTF-32 + LE      | `ff` `fe` `00` `00` |

BOMの利用は必須ではないが、これがあるとアプリケーションはエンコーディングをバイトオーダーまで機械的に判別することができる。

しかし反面、バイトオーダーそのものに加えて、BOMを付けるか否かというさらなる選択肢により、エンコーディングを一層多義的にしている点は否めない。[^2]

[^2]: 「UTF-16 BE」、「UTF-16 LE」のように言ったときに、特にBOMを伴わないUTF-16 + BE/LEの変換を指すことがある。（たとえば[Wikipedia](https://en.wikipedia.org/wiki/UTF-16#Byte-order_encoding_schemes)）
    またその場合、BOMを伴うものは、BEでもLEでも単に「UTF-16」という。

    前者においてバイトオーダーはエンコーディングというルールの一部であり、後者ではデータの一部（ルールは1つ）であると考えれば、理解できる用語法ではある。

    もちろん、その逆の意味で使われることもあるし、「UTF-16 BE with BOM」のように明示することも多い。
    冒頭のメモ帳では「UTF-16 BE」、「UTF-16 LE」はBOM付である。


## UTF-8 と BOM

上述の通り、各符号単位が1バイトに収まるUTF-8にバイトオーダーの概念はないのであるが、UTF-8であることを示すために、やはり先頭に「BOM」（あるいは「プリアンブル」）を付加することがある。[^3]

[^3]: この場合、バイト列としては `EF` `BB` `BF`となる。
    他のケース同様、`U+FEFF`をUTF-8に従って変換した結果である。

この扱い、あるいはその是非は、プラットフォームによっても若干異なる。
一般的には、これはUTF-8の特徴であるASCII互換性を損なうため、忌避される傾向にある。
また、多くのUnixシェルは、BOMがあるとshebangを認識しない。

他方で、一部のMicrosoft製アプリケーションは、あえてBOM付きのUTF-8を扱おうとするようだ。[^4]
たとえばExcelは、BOMの無いCSVをCP932（Shift JIS）でデコードしようとする。
その他、Visual Studioにテキストファイルを新規作成させるとBOMが付されていたりする。
（この辺の経緯を知っている人は教えてほしい。）

[^4]: そういったアプリケーションでは、よく「署名付きUTF-8」(UTF-8 with signature)という呼び名を使う。
    こう言うと何かセキュリティ絡みのようにも聞こえるが、単なるプリアンブルにすぎない。

## .NETでBOMを制御する

最後に、幾分具体的な例であるが、.NETアプリケーションにおいてUTF-8書き出し時のBOMを制御する方法を記す。
他のUTFについても、バイトオーダー自体の指定が加わる点を除いて同様である。

.NETでは各エンコーディングは`System.Text.Encoding`のインスタンスとして表現される。
特にUTF-8は、その子クラスである`System.Text.UTF8Encoding`のインスタンスであり、このコンストラクタは`bool`型の`encoderShouldEmitUTF8Identifier`を受け取る。 [^5]

[^5]: https://learn.microsoft.com/en-us/dotnet/api/system.text.utf8encoding

    リファレンスでは第2引数の`throwOnInvalidBytes`を明示的に`true`にすることが推奨されているが、本題ではないので省略。

これはその名の通り「エンコード時にUTF-8識別子（=BOM）を吐き出すか」を制御するフラグである。
デフォルトは`false`であるが、既存のインスタンスである`Encoding.UTF8`では`true`に設定されているので、BOM無しにするには自身でインスタンスを生成する必要がある。

```csharp
var usesBom = false;
var utf8WithoutBom = new UTF8Encoding(usesBom);
```

あとは、このインスタンスを`System.IO.StreamWriter`なりに渡してやればよい。

```csharp
using var writer = new StreamWriter("file.txt", utf8WithoutBom);
// ...
```

関連する注意点として、`String.GetBytes`の結果は、上記のフラグにかかわらず**常にBOMを含まない**。[^6]
そのため、Excelなどに配慮してBOM付きのUTF-8を書き出したい場合で、かつ`Writer`を介さずにバイト列を直接扱う場合、次のように自力で先頭にBOMを付加する必要がある。

[^6]: この動作自体は、得られたバイト列をさらに連結するような用途を考えれば理解できる。
    BOMはあくまでファイルの先頭に一度だけ付加するものである。

```csharp
var bytes = text.GetBytes(encoding);
var bytesWithBom = encoding.GetPreamble().concat(bytes).ToArray();
```

`Encoding.GetPreamble`は、上記のフラグに応じてBOM（バイト列）ないし空配列を返す。