ずとまよ暗号の文字化けした「鬆ｭ謨ｰ」はなぜ「頭数」になるのか

この記事は [この記事誰得？ 私しか得しないニッチな技術で記事投稿！ - Qiita](https://qiita.com/official-events/5d4f04cf2ba0cdbc8821) の参加記事です。

https://qiita.com/official-events/5d4f04cf2ba0cdbc8821

## はじめに

先日[とある謎解き](http://ihihi.me/) で、文字化けした`鬆ｭ謨ｰ`の文字列を戻す必要があることがありました（？）
ちょうど、[［改訂新版］プログラマのための文字コード技術入門：書籍案内｜技術評論社](https://gihyo.jp/book/2019/978-4-297-10291-3) を読んだので、練習として「鬆ｭ謨ｰ」がなぜ「頭数」になるのか解説を書きます。

（記事内に誤りがあればコメント等いただけると助かります）

## 前提

コンピュータでの文字の処理や描画には、[文字コード](https://ja.wikipedia.org/wiki/%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89)が関係しています。
コンピュータの内部では文字のそれぞれに番号が振られており、その番号を処理しています。
その文字と番号の組み合わせを定めたものが文字コードであり、様々な種類があります。そして、文字コードの種類ごとに文字と番号の組み合わせが異なったりします。

https://qiita.com/yuji38kwmt/items/b3a7820b4d3b544da4ff

例えば`ASCII`の場合、`Hello World!`という文字列は、以下のバイト列になります。

```ruby:irb
"Hello world!".encode(Encoding::ASCII).bytes.map { |c| sprintf("%#x", c) }
# => ["0x48", "0x65", "0x6c", "0x6c", "0x6f", "0x20", "0x77", "0x6f", "0x72", "0x6c", "0x64", "0x21"]
```

これを`UTF-16`で表すと ASCII とは異なるバイト列になります。（BOM は省略している）

```ruby:irb
"Hello world!".encode(Encoding::UTF_16BE).bytes.map { |c| sprintf("%#x", c) }
# => ["0", "0x48", "0", "0x65", "0", "0x6c", "0", "0x6c", "0", "0x6f", "0", "0x20", "0", "0x77", "0", "0x6f", "0", "0x72", "0", "0x6c", "0", "0x64", "0", "0x21"]
```

## 文字化けはなぜ起こるのか

[［改訂新版］プログラマのための文字コード技術入門：書籍案内｜技術評論社](https://gihyo.jp/book/2019/978-4-297-10291-3) では、文字化けのよくあるパターンとして以下 2 つが挙げられています。

### 文字コード A でエンコードされたものを文字コード B で解釈してしまうとき

例) `UTF-8`で保存したファイルを`Shift_JIS`として表示させた
例) `UTF-8`のデータの HTTP レスポンスヘッダに異なる charset が指定されていた

### 機種依存文字が含まれるとき

例) ベンダー A ではとある文字コードの空き領域に :qiitan: の文字を実装しているが、ベンダー B では同じ空き領域に :anger: を実装している場合、
「Qiitan かわいいよね :qiitan: 」と送ったつもりが、相手側では
「Qiitan かわいいよね :anger: 」と表示されてしまう

## 「鬆ｭ謨ｰ」はなぜ「頭数」になるのか

> 文字コード A でエンコードされたものを文字コード B で解釈してしまうとき

から、同じバイト列を異なる文字コードで解釈をすると「鬆ｭ謨ｰ」が「頭数」になるのではないかと考えました。

実際に実験をしてみましょう。

### 実験

今回は、`頭数`を`UTF-8`（現在一般的に使われている）で保存したファイルを、`Shift_JIS`（こちらも日本で広く使われている）として表示させてみます。

とりあえず、「頭数」を`UTF-8`でエンコードしたバイト列をみてみます。

```ruby:irb
"頭数".encode(Encoding::UTF_8).bytes.map { |c| sprintf("%#x", c) }
# => ["0xe9", "0xa0", "0xad", "0xe6", "0x95", "0xb0"]
```

これをファイルに保存し、`Shift_JIS`として解釈するようにファイルを開いてみます。

```console
$ od -tx1 atamakazu-utf8.txt
0000000    e9  a0  ad  e6  95  b0
0000006

$ nvim atamakazu-utf8.txt
```

![スクリーンショット 2023-07-16 15.19.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352836/297d8961-9d63-280b-eff2-a98e21df8694.png)

`頭数`が`鬆ｭ謨ｰ`になりました。

### 解説

もう一度、`頭数`を`UTF-8`でエンコードしたバイト列をみてみます。

```ruby:irb
"頭数".encode(Encoding::UTF_8).bytes.map { |c| sprintf("%#x", c) }
# => ["0xe9", "0xa0", "0xad", "0xe6", "0x95", "0xb0"]
```

`頭数`の文字はそれぞれ、

- `頭` -> `0xe9 0xa0 0xad`
- `数` -> `0xe6 0x95 0xb0`

のバイト列で表されています。

そして、`鬆ｭ謨ｰ`を`Shift_JIS`でエンコードしたバイト列もみてみます。

```ruby:irb
'鬆ｭ謨ｰ'.encode(Encoding::Shift_JIS).bytes.map { |c| sprintf("%#x", c) }
# => ["0xe9", "0xa0", "0xad", "0xe6", "0x95", "0xb0"]
```

`頭数`を`UTF-8`でエンコードしたバイト列と同じになっていることがわかります。

`鬆ｭ謨ｰ`の文字はそれぞれ、

- `鬆` -> `0xe9 0xa0`
- `ｭ` -> `0xad`
- `謨` -> `0xe6 0x95`
- `ｰ` -> `0xb0`

のバイト列で表されています。

つまり、`頭数`を`UTF-8`でエンコードしたバイト列が`Shift_JIS`で文字を表現できているため、このように文字化けになっています。

今回は謎解きなので考える必要はないので蛇足ですが、なぜこのような文字化けが発生してしまったかを真面目に考えるとすれば、

- `頭数`を`UTF-8`で保存したファイルを、誤って`Shift_JIS`として表示させた

のではないかということが推測できます。

## さいごに

文字化けした「鬆ｭ謨ｰ」はなぜ「頭数」になるのかについて書きました。
文字コードについて学んだ知識を活かす良いタイミングになったのでよかったです。

また謎解き楽しみにしてます。