Unicode の入力・表示テストに使える文字(列)

はじめに

アプリやゲームにおいて、「自由入力」のテキストボックスやそれを表示するラベル (ユーザ名・コメント欄・etc.) などを安易に設置すると、想定の斜め上を行く妙な文字(列)を入力され、意図しない表示・処理になることが多々あります。
本稿では、そのテストに使えるような文字(列)を記載していきます。なるべくコードポイント (U+xxxx) も併記するようにしました。

想定環境は主に PC ・スマートフォンで動作するクライアント (もっと具体的には Unity の TextMeshPro InputField) です。
Web やデータベース周りでは、例えば古典的な XSS寿司ビール問題 など別種の気を付けるべきポイントがあるかもしれませんが、あまり詳しくないのでここには記載しません。

なお、特に注記がない場合は Unicode / UTF-8 であるものとします。
環境によっては正しく表示されない可能性がありますがご了承ください。逆に考えると、その環境ではこれらのテストが有効であったことを意味します…
執筆環境は Windows 10 (游明朝が利用可能) / Firefox です。

省エネのため箇条書きスタイルでお送りしますが、ご了承ください。

テストに使える文字(列)

空文字列

  • それはそう

空白文字

  • U+0020 " " (半角スペース)
  • U+3000 " " (全角スペース)
  • U+0009 " " (Tab)
    • 環境によってサイズが大きく異なる
  • U+200B "" (ゼロ幅スペース)
  • その他 → スペース / 幅の比較
  • U+2800 "" (点字カテゴリの点がない文字)
    • 見た目上空白だが So (Symbol, Other) カテゴリのため、 Zs (Separator, Space) カテゴリを禁止してもすり抜ける
  • 空白文字のみで構成された文字列を許容するか?

改行文字

  • U+000D U+000A (CR LF), U+000A (LF) のどちらか?
  • 連打された場合にレイアウトが破綻しないか
    • あるいは、そもそも入力できるのが適切かどうか
  • 末尾の空行を行数にカウントするか

RTL (Right-to-Left)

  • 右から左へ描画するシステム (アラビア文字など)
    • العربية
      • U+0627 U+0644 U+0639 U+0631 U+0628 U+064A U+0629
      • データ上の並び順としては ‭ ا ـل ـعـ رـ ـب ـيـ ةـ となっている
  • U+202E (Right-to-Left Override)
    • 後続文字の描画順を強制的に「右から左」にする
    • ABCD と ‮DCBA
    • 上記 2 つは同じに見えるが、後者はデータ上 U+202E に続けて DCBA と並んでいる
    • 禁止ワードフィルタをすり抜ける、正しいファイル名に見せかけるなど大問題になりうる
      • "正規ファイルだよ‮xcod.exe‬" はデータ上 "正規ファイルだよ U+202E xcod.exe" であるので、実行ファイルとして開けてしまう
  • RTL と LTR の混在
    • 例: アラビア文字 (RTL) の文章中でも、数字は LTR (Left-to-Right) として描画されるべき
      • لدي 100 تفاحة.
        • == I have 100 apples.

その他の制御文字・特殊な文字

  • U+0000 "" ()
    • ヌル文字 \0
    • これが文字列終端になる環境 (C など) の場合、挿入することで後続の文字列を事実上消せる
  • U+0007 "" ()
    • ベル \a
    • ターミナルで出力すると音が鳴るなどする
  • U+FFFD (REPLACEMENT CHARACTER)
    • UTF-8 として不正なバイト列を読んだ場合などで出現
    • コピペなどで正規に(?)入力することもできるが、一般のユーザにはバグ・文字化けに見えそう

その環境における制御文字(列)

  • データベース (?) が csv の場合における ,"
  • Unity (TextMeshPro) における <b><s> , <color=red> など

長い文字列

  • テスト例
  • KB ~ MB オーダーの文字列を突然コピペされたときに動作するか
  • IME の変換前状態で、一時的にテキストボックスの文字数制限を突破できる場合がある
    • 毎フレーム処理したり、文字変更イベントを都度受け取ったりするタイプの環境では注意
  • 文字の正規化処理で長くなる可能性 → 正規化
    • U+FDFA NFKD ・ NFKC 正規化 で 18 文字に分解されうる
      • U+0635 U+0644 U+0649 U+0020 U+0627 U+0644 U+0644 U+0647 U+0020 U+0639 U+0644 U+064A U+0647 U+0020 U+0648 U+0633 U+0644 U+0645
      • صلى الله عليه وسلم
      • C# では "\ufdfa".Normalize(System.Text.NormalizationForm.FormKD) で確認可能
    • 正規化が必要な場合、正規化→長さチェックの順に行う (逆だと限界突破しうる)

大きなスペースをとる文字

  • 以下原寸大。特に最初の 2 つは入力・表示できてしまう環境が多いため注意
  • U+A9C5
  • U+FDFD
  • U+A9C1 U+A9C2 ꧁꧂
  • U+12031 𒀱
  • U+1242B 𒐫

大きなスペースをとる文字列

  • U+0E14 U+0E47 (x10) ด็็็็็็็็็็
  • U+05D1 U+059F (x10)
    • ב֟֟֟֟֟֟֟֟֟֟

スペースをとらない・不可視の文字

  • U+200B "" (ゼロ幅スペース)
  • U+200C "" (Zero width non-joiner; ZWNJ)
  • U+2063 "" (Invisible Separator) やほかにもたくさん
  • U+180E "᠎" (Mongolian Vowel Separator)
    • 上記と異なり General Punctuation ブロックではないので注意
  • なりすましや文字数稼ぎに利用されうる
    • アカウント名の末尾に追加して重複判定を回避する、4 文字以上必須のユーザ名を (見た目上) 空文字列にする、など
  • U+05D1 U+05BC (x10) בּּּּּּּּּּ
    • U+05BC を大量に重ねても幅をとらない

複数の表現がある文字 (正規化)

  • 同一の文字に複数の表現方法がある場合
  • これらが混在していると、検索・ソートなどを愚直に (バイト一致判定で) 行った場合に問題になる
    • 正規化 によってこれらを直感的に正しく行える
    • ただし、非可逆な操作なので保存する場合は注意

UTF-16 において 2 バイトで表現できない (U+10000 以降の) 文字

  • U+29E3D 𩸽 (ほっけ)
  • U+1F97A 🥺 など多くの絵文字が該当する
  • UTF-16 ベースで、サロゲートペアを正しく扱えないシステムにおいて問題になる
    • 例: C# において文字列長を string.Length でとっているとズレが生じる

フォントに含まれない文字

  • 明らかに文化の異なる文字は自明
    • U+1740 ブヒッド文字や U+10000 𐀀 線文字 B など
  • Unicode の比較的小さな部分にある漢字は機械的に弾きにくいかもしれない
    • U+34C7 など
    • JIS X 0213 の第 4 水準 あたりは対応していないものが多そう
      • 上の字は Adobe-Japan1-4 に対応している クレー に含まれないことを確認。ただ、当然一般的なものはほぼ含まれているので特殊な人名用漢字異体字でない限り実用上問題になることは少なさそう
  • もし対応文字数が少ないフォントを利用している場合は、デザインのずれが許容できるフォールバックフォントを用意するか、厳しめの (ホワイトリストのような) 文字制限をかける

異体字セレクタ

  • U+FE00 ~ U+FE0F, U+E0100 ~ U+E01EF を後ろにつけると文字の細かなバリエーションが変わる
  • 漢字
    • 葛󠄀 (U+845B U+E0100; 下部が "ヒ")
    • 葛󠄁 (U+845B U+E0101; 下部が "L + 人")
    • 邉 邉󠄀 邉󠄁 邉󠄂 邉󠄃 邉󠄄 邉󠄅 邉󠄆 邉󠄇 邉󠄈 邉󠄉 邉󠄊 邉󠄋 邉󠄌 邉󠄍 邉󠄎 邉󠄏 邉󠄐 邉󠄑 邉󠄒 邉󠄓 邉󠄔 邉󠄕 邉󠄖 邉󠄗 邉󠄘 邉󠄙 邉󠄚 邉󠄛 邉󠄜 邉󠄝 邉󠄞 邉󠄟
      • 最初の一文字 U+9089 がオリジナル (単体) で、それ以降は U+E0100 ~ U+E011F の異体字セレクタを後続させている (フォントの関係上すべて別の字形になっているとは限らないが)
      • 参照
  • 絵文字のスタイル選択
    • U+2615 異体字セレクタをつけて
      • U+2615 U+FE0E ☕︎ (テキストスタイル)
      • U+2615 U+FE0F ☕️ (絵文字スタイル)

国と地域の旗の絵文字

  • U+1F1EF U+1F1F5 🇯🇵 (日本の国旗)
    • 2 文字で表すタイプ → Regional indicator symbol
      • 🇯 と 🇵 (JP) からなる
    • 仕様上、検索などで問題になりうる
      • 🇰🇷🇺🇸 (韓国とアメリカ; KR US) のテキスト上で 🇷🇺 (ロシア; RU) を検索するとマッチしてしまう
      • 参考
  • U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F 🏴󠁧󠁢󠁥󠁮󠁧󠁿 (イングランドの旗)
    • 旗 + 地域コード(ここでは gbeng) + 終了 で表すタイプ → Emoji tag sequences

外字

  • U+E000 ~ U+F8FF, U+F0000 ~ U+FFFFD, U+100000 ~ U+10FFFD
  • そもそも入力できて良いのか?を検討すべき
  • 有名どころでは Apple の U+F8FF (林檎のロゴ) や Twitter の U+EA00 (例の鳥) など
  • それ以外の用例は → Private use areas
  • 特殊なところでは Ninja cat 🐱‍👤ニンジャキャット
    • U+1F431 U+200D U+1F464 🐱 + ZWJ + 👤
    • Windows 10 では 1 文字の Ninja cat 絵文字が描画される
      • 構成文字自体は一般的な (Unicode に存在する) ものだが、組み合わせたとき独自の描画になる

最近追加された文字

  • 2022/06/01 現在最新の Unicode 14.0 (2021/09) で追加された文字
    • U+1FAE0 🫠 (溶ける顔) など
  • 超マイナーな文字ならともかく、絵文字は比較的入力しやすいので危ない
  • 文字が追加される前提で設計しなければならない

複数のコードポイントで表現される文字

  • 👨🏻‍👩🏿‍👦🏽‍👦🏼ソース (図6)
    • 分割すると 👨 + 🏻 + ZWJ + 👩 + 🏿 + ZWJ + 👦 + 🏽 + ZWJ + 👦 + 🏼
    • U+1F468 U+1F3FB U+200D U+1F469 U+1F3FF U+200D U+1F466 U+1F3FD U+200D U+1F466 U+1F3FC
    • 1 書式素に 11 コードポイント、 UTF-8 では 41 バイト
      • 書式素 (Grapheme) : 人間が見て "1 文字" と思える単位 (要約)
    • これを正しく "1 文字" として判定するには、書式素単位でカウントできる StringInfo.GetTextElementEnumerator()GraphemeSplitter ライブラリなどを利用する
  • 👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩
    • U+1F469 U+200D を繰り返している。 35 コードポイント、 UTF-8 で 122 バイト
    • カーソルを動かす / 選択すると分かるように、これでも 1 文字扱いされる
  • 👩‍⚕️ (Woman health worker)
  • 👨🏿‍🦳
    • U+1F468 U+1F3FF U+200D U+1F9B3
    • 👨 + 🏿 (dark skin tone) + ZWJ + 🦳 (white hair)
      • skin tone の範囲は U+1F3FB ~ U+1F3FF
      • hair の範囲は U+1F9B0 ~ U+1F9B3
  • #️⃣
  • ஙொ
    • U+0B99 U+0BCA
    • U+0BCA は直前の文字を挟み込む形で作用する

色がある前提の絵文字・色を付ける絵文字

  • モノクロ描画の場合、識別が難しくなる可能性はある
  • 例 (上が通常、下は異体字セレクタ U+FE0E を後続させてテキストスタイルにしたもの)
    • U+26AA U+26AB U+1F534 U+1F535 U+1F7E0 U+1F7E1 U+1F7E2 U+1F7E3 U+1F7E4
      • ⚪⚫🔴🔵🟠🟡🟢🟣🟤
      • ⚪︎⚫︎🔴︎🔵︎🟠︎🟡︎🟢︎🟣︎🟤︎
    • U+2B1B U+2B1C U+1F7E5 U+1F7E6 U+1F7E7 U+1F7E8 U+1F7E9 U+1F7EA U+1F7EB
      • ⬛⬜🟥🟦🟧🟨🟩🟪🟫
      • ⬛︎⬜︎🟥︎🟦︎🟧︎🟨︎🟩︎🟪︎🟫︎
    • Unicode の字形例 ではハッチで描き分けている
  • 複数のコードポイントを利用する場合
    • U+1F408 U+200D U+2B1B 🐈‍⬛ (黒猫)
    • U+1F44F U+1F3FB 👏🏻 (skin tone)

合字 (リガチャ)

  • 連続すると見た目が変わる文字
  • コードポイント的には ae と æ (U+00E6) など
  • フォント(描画システム)側で解決する場合もあり
    • ffi / ffi
    • 前者がリガチャ無効、後者がリガチャ有効。コードポイントとしては同じ ffi
  • 文字体系がリガチャ前提な デーヴァナーガリー など
    • द्ध्र्य は、 द ् ध ् र ् य で構成されている
      • U+0926 U+094D U+0927 U+094D U+0930 U+094D U+092F
      • 参照

非常に似ている文字

環境に依存して見た目が変わる文字

  • ロケール
    • コードポイントはすべて U+9AA8
    • lang にそれぞれ zh-CN, zh-TW, zh-HK, ja-JP, ko-KR, vi-VN (中国, 台湾, 香港, 日本, 韓国, ベトナム) を指定している
  • OS
    • 絵文字は環境によって大幅に字形 (イラスト?) が異なる場合がある
    • U+1F3AB 🎫 (チケット)
      • 様々な環境での見え方
      • 確かに「チケット」だが、色合い・向きなどは多種多様
        • 色や向きなどに意味を持たせた表現をすると、別の表示になる受け手の解釈が変わってくる
        • どうしても統一したい場合は画像埋め込みか? (それでも IME とは表示が異なる)

1 文字あたりのバイト数が大きい文字

  • コードポイント単位
    • U+10000 以降の 4 バイトが最大 (UTF-8, UTF-16)
  • 書式素単位
    • イングランドの旗 🏴󠁧󠁢󠁥󠁮󠁧󠁿UTF-8 において 28 バイト
    • 家族の絵文字の無限連結 👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩‍👩 (制限なし)
  • 文字数制限がバイト・ワード単位でしか設定できない環境 (C#string.Length に依存している場合など) では、上記の文字を入力した場合に「見た目上の文字数」が著しく少なくなってしまう
    • 逆に、書式素単位で設定すると非常に大きいバイト数の文字を入力される可能性がある
  • 厳密に判定するなら書式素の数を数える。また、十分余裕を持ったバッファを用意しておく
  • 正規化で大きくなる文字 (前述)
    • U+FDFA は 1 文字から 18 文字に (UTF-8 では 3 バイトから 33 バイトに) 膨らむ

不正な文字列・バイト列

  • 解釈できないバイト列
    • UTF-8 における C0 AF
      • U+002F / を 2 バイトで表現した…ともとれるが、 /2F の 1 バイトで表現しなければならないため不正
      • これを / と解釈するとディレクトリトラバーサル対策の回避などに利用できてしまうため → セキュリティ
    • UTF-16 における D8 D8 (サロゲートペアの片割れ)
      • サロゲートペアを用いる文字を入力した際、一時的に現れる場合がある
        • char 単位でしか検証できない環境 (Unity の InputField.OnValidateInput) など
      • 文字数制限でペアの片割れだけ弾いてしまう、片割れのままログ出力してしまうなどでエラーになりうる
    • ただ、一般的な IME から直接入力・コピペする手段はないはず
    • 不正な部分は U+FFFD に置き換えられうる (VSCode で確認)
      • 環境によってはより深刻なエラーにもなりうるため、存在させない
  • 非文字
    • 文字ではないコードポイント (未定義とは異なり、将来的にも文字になりえない)
    • U+FFFE
      • (UTF-16 において) U+FEFF (BOM) のバイト順を逆に解釈するとこれになる。したがって、これを認識した場合はバイト順を逆にして読むべきであると分かる
      • 補足: BOM → Byte Order Mark
        • UTF-8 において EF BB BF
        • ただし、 UTF-8 においては推奨はされない (あってもいいが、 UTF-8 はそもそもエンディアンに依存しないため入れる必要がない)
    • 「存在してはいけない」わけではなく、内部的に使用されることはある (最も、外部とのやりとりに使うべきではない)
  • 未定義の文字

余談

上記に載せるほどではない、文字関連の余談です。

幽霊文字

  • 実際の利用例がない文字 → Wikipedia
  • プログラム的には気にしなくてよい。しいて言えばフォントが対応しているかぐらい
    • その定義からして使われることはまずないが…
  • マシュマロでは U+5F41 彁 を伏せ字に利用している
    • 利用例がないことを逆手に取っている → 参照

点字

  • U+2800 ~ U+28FF → Wikipedia
  • ドットアートが可能 ⠠⠵
  • U+2800 "" は見た目上空白だがカテゴリが Other Symbol のため、空白カテゴリを禁止してもすり抜ける可能性がある (上述)

Methematical Alphanumeric Symbols

  • 𝑰𝑵𝑻𝑬𝑹𝑵𝑬𝑻 𝑨𝑵𝑮𝑬𝑳
  • 文字だけで (フォントを変えることなく) 書体を変えることができる → リスト
  • A 𝐀 𝐴 𝑨 𝖠 𝗔 𝘈 𝘼 𝒜 𝓐 𝔄 𝕬 𝙰 𝔸
    • U+0041 U+1D400 U+1D434 U+1D468 U+1D5A0 U+1D5D4 U+1D608 U+1D63C U+1D49C U+1D4D0 U+1D504 U+1D56C U+1D670 U+1D538
  • 基本的にコードポイントは連続しているが、一部既に存在した文字 (U+210E など) の箇所は未割り当てになっているので注意 (前述の に対応する U+1D455 など)
    • また、既存だった文字は書体がずれがちなので注意
      • ... 𝑓 𝑔 ℎ 𝑖 𝑗 ...
      • きちんとやりたいならフォントを変えるべき

実際に問題を起こした文字列

  • Black dot bug
    • iOS で特定の文字列を表示するとアプリがクラッシュしていた → 参照
    • U+0020 U+003C U+26AB U+003E U+0020 U+1F448 U+1F3FB U+0020 (U+200E U+200F)x891 U+0020 U+200E
      • gist
      • LTR と RTL が非常に多く含まれている
    • Apple でさえ、バグるときはバグる
  • テルグ文字 (の一部の組み合わせ)
    • 同じく iOS でクラッシュしていた → 参照
    • U+0C1C U+0C4D U+0C1E U+200C U+0C3E
      • pastebin
      • Black dot bug とは異なり短いが、壊れるときは壊れる

ひらがなは 2 バイト文字か?

  • UTF-8 において "2 バイト文字" ではないのは自明。 2 バイトなのは U+07FF までで、平仮名ブロックは U+3040 から始まる
  • では「すべてのひらがなは 3 バイト」と言えるか?
  • U+1B001 𛀁 (や行え) はひらがなであり 4 バイト (UTF-8F0 9B 80 81) なので、つまり一概には言えない

おわりに

以上のように、個性豊かな文字(列)がたくさん存在します。
たかが文字、されど文字…
全部 Unicode が悪い… と思いましたが文字は実世界の映しではあるので、つまり人類が悪い
また、本稿は問題になりうる文字を網羅したものではなく、あくまでそれらの一部です。

テキストボックス実装の際は、全部対応… するのは骨が折れるどころか複雑骨折するので、何らかのライブラリを利用するか、目立つところは処理してあとは心の隅に置いておくか、とするとよさそうです。

コピペが簡単になるように以上をまとめたテキストファイルをご用意いたしました。
上述の文字(列)たちを LF 改行で記載しています。テストにご利用ください。
※ U+0000 (ヌル文字) だけは gist に入力できなかったため省略しています。ご了承ください。

Unicode の入力・表示テストに使える文字(列) (U+0000 null を除く)