Unicode の正規化でハマった話

まずは以下のコードを見てください。

import java.io.File
import java.io.FileOutputStream

val file1 = new File("\u30C9ラえもん.txt")
val fos1 = new FileOutputStream(file1)
fos1.write("ぼく、ドラえもん。".getBytes("UTF-8"))
fos1.close()

val file2 = new File("\u30C8\u3099ラえもん.txt")
val fos2 = new FileOutputStream(file2)
fos2.write("ぼく、ドラえもん!".getBytes("UTF-8"))
fos2.close()

本エントリとは関係ないですが、こうしたちょっとしたコードを書くのに Scala は非常に良いですね。

さて、「ドラえもん.txt」というファイルを 2 回作成しようとしています。わかりづらいですが、ファイル内の最後が「。」なのか「!」なのかの違いがあります。

最初に作成しようとしているファイルの「ド」は、普通の「ド」、次に作成しようとしているファイルの「ド」は、「ト」+「゙」の結合文字列です。この場合、「ト」を基底文字、「゙」を結合文字と呼びます。

これを Mac で実行すると、「ドラえもん.txt」というファイルがひとつだけ作成されます。最初に作成されたファイルを、2 番目に作成したファイルで上書きした形です。

これを Windows で実行すると、「ドラえもん.txt」と「ドラえもん.txt」というファイルが作成されます(片方は単一のド、片方は合成文字列のドです)。

また、生成されたファイルを Mac なら:

ls > ls.txt

Windows なら:

chcp 65001
dir /b > dir.txt

とリダイレクトすると、Mac なら分解されて、Windows ならそれぞれ適切に結果に保存されます(Windows はchcp でコマンドプロンプト文字コードUTF-8 に変換する必要があります)。

MacWindows でこうした結果になるのは、それぞれのプラットフォームで Unicode の正規化の仕組みが異なるからです。

Mac は NFD (Normalization Form Canonical Decomposition) という正規化形式が使用され、これは合成文字列を分解して正規化するのに対し、WindowsNFC (Normalization Form Canonical Composition) という、分解した後結合して正規化するためです。

このような違いにより、どのような問題が発生するでしょうか。


Windows から合成文字列のファイルを Mac に持ってくると、Mac では扱えなくなります。

具体的には、該当ファイルは Finder で表示されません。ターミナルで cp などを行おうとしても、ファイルが存在しないと言われエラーが発生します。もし、ふたつのファイルが同一フォルダに存在する場合、見えるはずの合成文字列ではないファイルも Finder から見えなくなります単に合成文字列のファイルは見えなくなります。

上図は、Finder ではひとつしかないけど、ターミナルからはふたつ見えています。

Mac 上では合成文字列の「ドラえもん.txt」は適切に扱えないのに、何故かリダイレクト結果は合成文字列なので、リダイレクト結果を元にプログラミングをすると、期待通りに動作しない可能性があります。

Windows では、合成文字列ではないファイル名で検索すると、合成文字列ではないファイルだけがヒットし、合成文字列のファイル名で検索すると、検索結果にヒットしません。

合成文字列でなければ見つかります。

合成文字列だと見つかりません。

ファイル名は何かの拍子で合成文字列版に変わってしまうことがあり(おそらく私が WindowsMac で使用している ZIP ツールか、受け渡ししている相手の Windows の ZIP ツール)、ファイルはあるのに検索しても見つからない、ということが発生します。見つからなかったことで、お客様相手に、送った・受け取ってない、みたいな問題になると大変です。


MacWindows でのファイルの受け渡しに関するトラブルは、今に始まった話ではなく、過去は過去で Mac バイナリや文字コードに関する注意が必要でしたが、今どきは Unicode に統一されてハッピーという訳にはいかず、やはり気をつけていかなければならない、というお話でした。

補足:

Dropbox は、事前に Unicode エンコードの競合を検知してくれる機能があり、ファイル名を「ドラえもん (Unicode エンコードの競合).txt」などと変更してくれます。

LinuxWindows と同じ正規化形式なので、MacLinux でも同様の問題が発生する可能性があります。

最近のエディタは、合成文字列を一文字みたいに扱う傾向があります。Windows のメモ帳、Mac のテキストエディット、TextMateVim などなど。僕は秀丸のように、常に分解された状態で編集できるエディタが好みですね。