HFS+のエンコーディングとUnicode正規化
第3版


第1版 2012/12/26
第2版 2014/7/10 全面的に書き直し。
第3版 2015/10/4 全面的に書き直し。Thnaks! hiroさん


追記:コメントもご覧ください。第4版書かなきゃ…

HFS+のテキストエンコーディング

HFS+はファイルやフォルダなどのアイテム名文字列をどのエンコーディングで扱っているのでしょうか? 実はこれを明確に記載したドキュメントをAppleは最近まで公開していたのですが、今はありません(2015年10月現在)。それでも第三者によるアーカイブがかろうじて残っており、典拠として貴重なのでここに記録しておきます。
2009年時点のFile Systems and Unicode Support
Apple - File Systems and Unicode Support

見ての通りUTF-16ですね。インターネット上ではUTF-8-MACであるとの説明が散見されますが、誤解が生んだ風説で間違っています。

UTF-8-MACは、iconvが実装したテキストエンコーディングです。これは規格として定められたものではありません。UTF-8の変換マッピングにHFS+固有のUnicode正規化を加味したいわば野良エンコーディングです。iconv以外のツールにもUTF-8-MAC相当のエンコーディングを実装しているものがありますが、いずれもiconvの野良エンコーディングの模倣です。

HFS+のUnicode正規化

Unicode正規化形式はUAX#15で4種類が正式に決められています。HFS+はそのうちのNFDをさらにAppleが改変した特殊な正規化形式を実装しています。アイテム名文字列はHFS+に保存される時点で一律にこの正規化形式が適用されます。

この特殊な正規化形式に名称はありません。ここではApple Modified NFDと呼ぶことにします。ちなみに、これを便宜的にでもUTF-8-MACと呼んではいけません。そもそもUTF-8ではないですし、Unicode正規化はエンコーディング変換ではなく、テキスト置換だからです。

Appleの改変は、一部の文字だけ分解しないようにしたことです。ここでいう「分解」はUnicode正規化の専門用語で、1文字を複数文字(結合文字列)に置換するだけでなく、1文字を別の1文字に置換する意味も含みます。Apple Modified NFDが純正のNFDと異なるのは、1文字を別の1文字に置換しないことです。

Apple_Modified-01

Apple Modified NFDがどの文字を別の1文字に置換しないのか、その符号位置範囲は上記ドキュメントに記載されているものの、古いので今は不正確です。より正確な記載はAppleのドキュメントにあります。
Precomposed versus Decomposed
Apple Modified

  • U+2000–U+2FFF
  • U+F900–U+FAFF(CJK互換漢字)
  • U+2F800–U+2FA1F(CJK互換漢字追加)

なお、この符号位置範囲はざっくりとブロックで示しているだけで、純正のNFDがこのすべての文字を分解するわけではありません。たとえば、U+2000–U+2FFFで実際に分解されるのは8文字のみです。上記の範囲は、Apple Modified NFDが分解の対象にしない範囲として必要十分なものです。

プログラミングでの注意

HFS+上のアイテム名をプログラミングでリネームするとき、リネーム後のアイテムの取得に注意が必要です。「リネームで使用する文字」と「リネーム後の実際の文字」が、Apple Modified NFDの影響で一致しない場合があるからです。

リネーム前後の文字を一致させる手っ取り早い解決方法は「リネームで使用する文字」を先にApple Modified NFDに相当する処理で変えておくことでしょう。しかし、ここでもさらに注意が必要です。「CJK互換漢字追加」はApple Modified NFDでたしかに分解されませんが、この文字が実際にアイテム名にあると、そのアイテムを移動、削除、リネームしようとしてもアラートが表示されてなにもできなくなります。原因は不明ですが、OS Xのファイルマネージャーのバグかもしれません。したがって、リネームで使用する文字はCJK互換漢字追加を分解して統合漢字に変えておくのがベストです。

・安全なApple Modified NFDの処理例:download – safetyAppleModifiedNFD.pl

さらにプログラミングでは、アイテム名文字列を合成(結合文字列を1文字に置換)したいときがあります。ここでも注意が必要で、純正のNFCを適用してはいけません。Unicode正規化はかならず分解をしてから合成をするので、NFCは合成だけでなく、NFDと同じく1文字を別の1文字に置換してしまいます。分解による1文字へ置換はその後に合成しても元に戻らないので、とくに「合成除外」と呼ばれます。

そこでApple Modified “NFC” に相当する処理をしたくなるのですが、上記の符号位置範囲は合成除外をすべてカバーしているわけではありません(ここがややこしい)。

Apple_Modified-02
Apple Modified “NFC” が分解する合成除外 110文字

アイテム名文字列だけならいいのですが、一般のテキストをApple Modified “NFC” で汎用的に処理するのは問題があります。いずれの場合も「合成除外をすべて分解しない安全なNFC」で処理するのがベストです。

・安全なNFCの処理例:download – safetyNFC.pl


上記のダウンロードファイルは、ひらくんさんが作ってくれた「正規表現にマッチする文字のみUnicode正規化を適用するPerlスクリプト」を利用して作成したものです。Perlをある程度読み書きできる方向けです。とても簡潔に書かれているので、使い方はコードを見れば普通に分かると思います。

ちなみに合成除外はUnicode 6.1以降変更はありません(Unicode 8.0現在)。

「野良エンコーディング」とあえてネガティブな表現をしたのは、UTF-8-MACの誤解があまりに広範で根強く、このくらい強い表現にしないと伝わらないんじゃないかとの危機感です。エンコーディングにUnicode正規化を織り込むのは決して筋が悪いわけではありません。なにしろ便利ですから。いいかえると、UTF-8-MACがあるのは便利だから以上の理由はないのです。

14件のコメント

  1. ものかのさん、昨日はお世話になりました!
    昨日なんとか分かった部分をMacでいじって確認しています。
    それで、急にスミマセンが、ご無理のない範囲で教えてください~

    これ、最大の問題は、U+2F81A(

  2. ※文章が区切れてスミマセン; サロゲートペアの冬(下のニがン、U+2F81A)で途切れたみたいです (^^;

    ものかのさん、昨日はお世話になりました!
    昨日なんとか分かった部分をMacでいじって確認しています。
    それで、急にスミマセンが、ご無理のない範囲で教えてください~

    これ、最大の問題は、U+2F81Aの「冬(下のニがン)」のようなSIP面の合成除外文字が、文字化けを避けるためにMacのHFS+特有のmodified NFDの適用を免れるはずなのに、なぜか分解されてしまう、ということですよね?

    私はU+2F81Aを文字ビューアで探し、新規フォルダ名に入力してみましたが、難はあったものの「冬(下のニがン)」というフォルダが出来ました。
    難というのは
    (1)名前をつけた瞬間に「操作を完了できません。予想できないエラーが起きました(エラーコード -43)」というエラーが出た
    (2)一度できたフォルダを変名できなくなった(同じエラー)
    (3)フォルダをゴミ箱に入れられない(「いくつかの必要な項目が見つからなかったため、操作を完了できません(エラーコード -43)」)
    (4)問題のフォルダを包含する親フォルダをゴミ箱に入れると、入ったが、ゴミ箱を空にしようとすると、親フォルダと問題のフォルダが削除できない

    結局シェルからrmdirすると消えました。

    ということで、問題はあったんですが、その原因が「modified NFDが適用されてしまうこと」かどうか、私には判断がつかなかったのです。

    というのは、ファイル名をクリップボードにコピーして、CotEditorにペーストして、UTF-32BEで保存し、filedumpすると、正しく「00 02 F8 1A」1文字になっていたのです。
    (「が」で同様の操作を行うと、00 00 30 4b 00 00 30 99の2文字に分裂する)

    U+2F800~U+2FAFFの範囲が、分解対象に入っているとものかのさんが思われた理由を教えていただけるでしょうか。
    お忙しいところスミマセン!

    あと、もう1つ質問です。
    Unicode Normalization Testをダウンロードさせていただき、起動しましたが、レインボーボールがクルクル止まらなくなります。
    これは何かわかるでしょうか。

    環境は
    MacBook Air 11-inch
    Late 2010
    CPU: Intel Core 2 Duo 1.4 GHz
    Mem: 2 GB 1067 MHz DDR3
    GPU: NVIDIA GeForce 320M 256 MB
    OS: Mac OS X Lion 10.7.5 (11G63)
    です。

    よろしくおねがいします!

  3. 深沢さん、文字っ子集合で面白かったですね〜

    > U+2F800~U+2FAFFの範囲が、分解対象に入っているとものかのさんが思われた理由を教えていただけるでしょうか。

    これについては、コメントではなく、ブログ記事として書こうと思ってます。お楽しみに!

    > Unicode Normalization Testをダウンロードさせていただき、起動しましたが、レインボーボールがクルクル止まらなくなります。

    ごめんなさい!! 古いバージョンのまま放置してました…
    もうちょっと作り直してから新しいものを公開しようと思います。

  4. ターミナル等滅多に使わないプログラム音痴です。
    「modifiedNFD_Measure.pl」
    これの具体的な使用方法を教えて下さい。
    よろしくお願いいたします。

  5. えぞおこじょさん
    それはPerlのコードを読み書きできる方向けのものです。
    使い方はコードを見て理解してくださいとしか言えません。
    そのまま使える場合もあれば、カスタマイズが必要になる場合もあります。
    そこまでサポートできないんです。ごめんなさい!

  6. 記事の内容についてではありませんが、報告させていただきます。
    このブログのHTMLが誤っており、そのために正しいタイトルが表示されていません。
    具体的には、10行目にあるmeta name=viewport要素のcontent属性が半角の「”」ではなく全角の「”」で閉じられており、そのために直後にあるtitle要素が適切に認識されていない状態です。
    修正を行うべきではないかと考えます。

  7. sounisi5011さん
    ありがとうございます!
    この記事だけではなく、ブログ全体がそうなっていました。教えていただき、助かりました!

  8. 素晴らしい記事をありがとうございます。
    細かい事ですが、何点かだけ確認させてください。

    まず合成除外の説明に違和感を感じました。
    合成除外は、純正のNFDでbaseとcombiningに分解するが純正のNFCでは合成しない文字のことで、「1文字へ置換」ではないと思います。
    (その後の例などからものかの様もわかってはおられるとは思いますが、あくまで説明への指摘として)

    それと、Unicodeのバージョンにも触れていただければと。
    HFS+が作られたのが2000年前後で、最新のUnicodeに準拠したライブラリを使ってしまうと若干の食い違いが出る恐れがあります。
    実際にOSが用いているデータは http://www.opensource.apple.com/source/xnu/xnu-1228.9.59/bsd/vfs/vfs_utfconvdata.h で見えます。

    それから、プログラムでの扱いですが、CoreFoundationのCFStringGetFileSystemRepresentation(NSStringのfileSystemRepresentation)に任せてしまうのが確実で、独自に変換するのは細かい挙動を模倣できず悪手です。HFS+には正規化以外にも妙なルール(illegal sequenceを%で置き換える等)がいっぱいありますし……。

    愚痴愚痴と申し訳ありません。

  9. ytさん、ご指摘ありがとうございます!

    > 「1文字へ置換」ではないと思います

    私の説明、辻褄が合ってないし、Apple Modified NFCの図もよくないですねえ。書き直さなくちゃ。

    > Unicodeのバージョンにも触れていただければと

    この点、実はよく分からないことになっていて。
    記事冒頭のFile Systems and Unicode Supportでは
    「U+F900–U+FA6A」
    その次のPrecomposed versus Decomposedでは
    「U+F900–U+FAFF」
    このように範囲が異なります。
    で、その差の中にあるU+FA6B–U+FA6Dの3文字は、Unicode 5.2(2009年)で追加されたものなんですけど、Finderでアイテム名にしてみると分解されません。

    記事であえて出さなかったTN1150にはUnicode 2.0と記載。
    記事冒頭のFile Systems and Unicode SupportにはUnicode 3.2と記載。
    でも今はUnicode 5.2で追加された互換漢字は分解されない。
    なのでUnicodeのバージョンはどうにも触れられない状態で悩ましい…。

    > CoreFoundationのCFStringGetFileSystemRepresentation(NSStringのfileSystemRepresentation)

    「リネームする前に文字列をApple Modified NFD相当で分解しておく」の下りですね。
    この関数、知りませんでした…この程度のレベルですw
    CFString/NSStringで返してくれればもっと楽なのにと思ってしまうレベル。

    「野良正規化」はそれでもPerlのようなスクリプト言語で必要になると思うんですね。「プログラミングでの注意」といいつつ、勝手にそっちの方を念頭に置いていました。

  10. ご返信ありがとうございます。

    >分解されません

    Unicode 5.2で追加された文字が2.0〜3.2前後を元にしたテーブルで分解されないのは当然ではないでしょうか。その頃はそんな文字なかった、ということで。

  11. あれ?

    その頃はそんな文字なかった
      ↓
    分解の対象になっているので、分解されて統合漢字に変わるはず
      ↓
    でも現状は分解されず、互換漢字のまま。分解の対象外になっている

    ということで、むしろUnicode 5.2が反映されていると考えたのですけど。

  12. そっか。
    Apple Modified NFDがしているのは「分解しない範囲以外を分解する」ではなく、
    「分解する符号位置を決めて分解している」んですね。

    つまり、分解する符号位置以外はすべて分解しない。
    分解する符号位置は当時のUnicodeバージョンの範囲内にある。
    わーいろいろ難しいなこれ。

  13. > UTF-8-MACは、iconvが実装したテキストエンコーディングです

    ちょっと表現が微妙で、どう微妙かを説明すると長くなるんですが、
    そもそもOS XやLinuxなどのUnix系のOSは、まずUNIXの標準仕様があり、それを各々が実装するというかたちになっています。
    (歴史的経緯や、政治力の綱引きなどの事情で標準仕様が後追いになることもある…というか大半が後追いなんですが、この辺の雰囲気はW3C仕様とブラウザの関係と似ているのであんな感じだと思ってください)

    で、その仕様の一つにiconvがあり、OS X版のiconvにはAppleがUTF-8-MACを実装しているということになります。
    LinuxやSolaris、AIXなど、別のOSにはそれぞれ別のiconvがあって、対応するエンコーディングも異なっています。

  14. わー成瀬さんだ、こわい!(笑

    > OS X版のiconvにはAppleがUTF-8-MACを実装
    UTF-8-MACがOS X版iconvの独自エンコーディングってことを失念していました。
    Appleが実装したんですね。そうすると野良って言いすぎですねぇ。
    iconvがBMP外で文字化けしなければ一押しなんですけど…。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です