ファイル名を判定する際は正規化に気をつける
ブラウザからアップロードされたファイルを、とある命名規則に沿っているか判定する処理を実装していました。 しかし、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のように分解したあと視覚的・意味的に等価な文字列を合成し合成文字として扱います。 このように視覚的には違いがわからなくても、“ヘ”+"゜“の結合文字と"ペ"の合成文字は異なるコードポイントとして扱われるため、比較結果が異なってしまいます。