ブラウザからアップロードされたファイルを、とある命名規則に沿っているか判定する処理を実装していました。
しかし、MacとWindowsで一貫した挙動にならず、悩まされた結果、unicode正規化の問題であることがわかりました。
この記事では、その問題と解決策について共有します。
ハマった事象
正規表現でファイル名の判定を書いていたのですが、簡単のために比較表現で説明します。
下記のような判定をしていたのですが、後者の判定結果がfalseを返してしまいました。
"ペ" == "ペ"
=> true
"ペ" == "ペ"
=> false
原因
MacのFinderでは、ファイル名に含まれる文字がunicode正規化のNFD(Normalization Form D)形式で保存されます。 しかし、エディタに入力した文字はNFC(Normalization Form C)形式であるため、同じ見た目の文字列でもバイト列が異なり、不一致となってしまいます。
false判定になった文字列で、それぞれの文字コードを調べると以下のようになります。
"\\u%04x" % "ペ".ord
=> "\\u30da"
"\\u%04x" % "ペ".ord
=> "\\u30d8"
このようにコードポイントが異なるため、等価ではないと判断されてしまいます。
後者の(\u30d8)は、Finder上でフォルダを作成したときに生成される"ペ"の文字コードで、これは基底文字"ヘ"(\u30d8)と結合文字"゜"(\u309c)の組み合わせで表現されています。
解決策
このようにコードポイントの違いで比較結果が期待通りにならない場合、文字列を正規化した上で比較を行うことで、解決できます。
pe = "ペ"
"\\u%04x" % pe.ord
=> "\\u30d8"
"ペ" == pe
=> false
"ペ" == pe.unicode_normalize(:nfc)
=> true
このようにRubyで用意されているunicode_normalizeメソッドを使用して、NFC形式に正規化することで、期待通りの比較が可能になります。
MacのファイルシステムではNFD形式で保存されるため、ファイル名を扱う際にはNFC形式に正規化してから比較や処理を行うことをお勧めします。
unicode正規化とは
Unicode正規化wikiに詳しく記載されていますが、NFDとNFCの違いを簡単に説明します。
まず、合成と分解という概念があって、NFDは視覚的・意味的に等価な文字列に分解し結合文字として扱います。 一方、NFCはNFDのように分解したあと視覚的・意味的に等価な文字列を合成し合成文字として扱います。
このように視覚的には違いがわからなくても、“ヘ”+"゜“の結合文字と"ペ"の合成文字は異なるコードポイントとして扱われるため、比較結果が異なってしまいます。