ブラウザからアップロードされたファイルを、とある命名規則に沿っているか判定する処理を実装していました。

しかし、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のように分解したあと視覚的・意味的に等価な文字列を合成し合成文字として扱います。

このように視覚的には違いがわからなくても、“ヘ”+"゜“の結合文字と"ペ"の合成文字は異なるコードポイントとして扱われるため、比較結果が異なってしまいます。