FSRefs と Unicode ロングファイル名

FSRef からアイテム名を取得する方法は?

inline OSErr FSRefGetName( const FSRef &fsr, HFSUniStr255 *name ) 
{
    return FSGetCatalogInfo( &fsr, kFSCatInfoNone, nil, name, nil, nil ); 
}

HFSUniStr255 は以下のように定義されています。

struct HFSUniStr255 { 
    UInt16 length; /* number of unicode characters */ 
    UniChar unicode[255]; /* unicode characters */ 
}; 

HFSUniStr255 は 512バイトの領域を消費するので、CFStringRef に格納するようにした方が良いでしょう。

strRef = CFStringCreateWithCharacters( kCFAllocatorDefault, name.unicode, name.length ); 

これには、メモリ領域の節約という意味もあるけれど、Core Foundation は CFString を操作する有益な API をいろいろと提供しているという理由も大きいです。HFSUniStr255 ファイル名に関しては、そういった操作用の API は提供されていないのです。(さらに言うと、CDString のラッパークラスを作って、CFStringRef/CFMutableStringRef の両方を扱えるようにしてみることを強くお勧めします)

FSGetCatalogInfo() はファイルシステム名を返すということに注意しておきましょう。Mac OS X の Finder はファイル名の拡張子部分を常に表示しているわけではないのです。ユーザはこれに関する設定を自分で行える箇所もあるし、例えば .application 拡張子のように Finder では絶対に表示されないものもあるのです。Finder が表示するのに使う名前を表示名と呼んでいて、Mac OS X では次のようにすると CFStringRef として取得できます。

err = LSCopyDisplayNameForRef( &fsRef, &cfString ); // (LaunchServices.h を参照のこと) 

他の Launch Services API 群と同様、LSCopyDisplayNameForRef() は Mac OS 9 では使用できませんが、Mac OS 9 の Finder は常にファイルシステム名を表示しているので、問題にはならないでしょう。

注意: 技術的に正確を期するなら、HFSUniStr255 の定義は少々間違っています。HFS+ ではファイル名は正規形D (NFD) を Apple が改変した形式の UTF-16 として保持しています。これは Unicode のコードポイント 1つが、HFSUniStr255 中の UniChar 2つ分以上を使う可能性があるということです。すなわち、ファイル名長の制限は255文字よりももっと少ないかもしれないということです。("Unicode 簡単メモ" 中の「用語」の項を参照)

訳注:また、Iimori 氏による「UnicodeUtils OSAX 」のページの記述も大変参考になります。

Unicode 文字列に関する覚え書き

厳密に言うと、ここに記述する内容は CFString に依存した内容ではないのですが、Unicode ファイル名を扱う場合によく直面する問題でもあるので、敢えて取り上げることにします。

アプリケーションを作っていると、ファイル名やフォルダ名を表示する場面がよくあります。Mac OS X が Unicode のロングファイル名をサポートして以来、Carbon-Development メーリングリストでもそれに関する話題が度々出てきたし、そのうちのいくつかには私もフォローコメントを投稿してきました。けれど、ここでいったんこれまでの知識を覚え書きにしておきます。もし Unicode についてあまり詳しくないのなら、その扱いには少々クセがあることを知っておきましょう。では、Unicode ファイル名を扱う場合の基本を示します。

Unicode 文字列は(Mac OS X においては)UniChar の文字列で、CFStringRef または CFMutablaStringRef と相互変換が可能です 。

Unicode の 1コードポイントを表現するのに複数の UniChar を必要とするかもしれないので、任意のオフセット値と範囲を指定して単純に削除/挿入操作をしてはいけません。 そのような処理をすると、予想外かつ正しくない文字列になってしまう可能性があるだけでなく、Unicode 文字列として無効なものとなることさえあります。

幅によって切りつめる

描画の為に幅を指定してファイル名を切りつめる場合には、CFMutableStringRef に変換してから TruncateThemeText() を使います。

Boolean TruncateWidth( 
    CFMutableStringRef ioString,
    SInt16 inMaxWidth,
    TruncCode inTruncCode, 
    ThemeFontID inThemeFontID,
    ThemeDrawState inState ) 
{
    Boolean wasTruncated = false; 

    OSStatus err = TruncateThemeText(
         ioString, inThemeFontID, inState, inMaxWidth, 
        inTruncCode, &wasTruncated 
    ); 

    return wasTruncated; 
}

長さによって切りつめる

残念ながら、Unicode 文字列を、文字の数によって正しく切りつめるための簡単な API は存在していません。長さによってファイル名を切りつめるには、UniChar 文字列に変換してから UCFFindTextBreak() を kUCTextBreakClusterMask フラグとともに呼び出します。 (これはちょっとした長さのコードになるので、本稿では要点だけ提供します ;-) )UCFFindTextBreak() を使うと、クラスタの途中で文字を分断しないことを保証できます。 クラスタとは複数の UniChar によって構成される、意味上の最小の単位です。クラスタから 1文字以上を削除してしまった場合、意味が変わってしまうのは良い方で、最悪の場合 Unicode として不正なものとなってしまいます。 ("Unicode 簡単メモ" 中の「書記素クラスタ」を参照)

連結

Unicode 文字列を連結することができます。個々の文字列はそれぞれ意味を持ち続けます。例えば、既存の文字列にその意味を変えることなく、".txt" という Unicode 文字列を追加することができるのです。

文字列の幅の取得

Unicode 文字列の幅を、UniChar の数に基づいて概算してはいけません。加えて、combinging 文字とサロゲート・ペアの問題もあります。つまり、Unicode テキストは表示対象とならない不可視の文字を含みうるのです。Unicode は単純なスクリプト/文字集合エンコーディングの世界からはほど遠い部分があるわけです。Unicode コードポイントの中には、それ自身は表示対象にはならないけれど、アプリケーションに対して表示方法のヒントを示すために使われるものもあります。CFString として保持されている Unicode 文字列の幅を求めるには、以下のようにします。

SInt16 GetWidth(
    const CFStringRef inString, ThemeFontID inFontID, ThemeDrawState inState 
) 
{
    Point pt;
    SInt16 baseline; 

    GetThemeTextDimensions( inString, inFontID, inState, false, &pt, &baseline ); 

    return pt.h; 
}

ファイル名のエンコード方式変換

繰り返しになるけれど、ここで書く事柄はファイル名に限った話ではありません。けれど、CFString を C 文字列に変換する時に必要なバッファサイズを CFStringGetSize( cfString ) で取得した値で仮定してしまう間違いが、非常にしばしば発生するので取り上げることにします。kCFStringEncodingUTF8 でエンコードされて格納されている文字列の 1 UniChar が変換時に必要とする文字数は(単純な Latin 文字を除けば) 2文字以上になる可能性があります。したがって、次に示すように CFStringGetMaximumSizeForEncoding() を使って必要なバッファサイズを決定するのが正しい方法です。

char* CreateUTF8CStringFromCFString( const CFStringRef inString ) 
{
    // ForgetPtr( cStr ) must be called when with the result of this call if it is successful. 

    CFIndex len = CFStringGetLength( inString ), 
            maxBytesNeeded = CFStringGetMaximumSizeForEncoding( len, kCFStringEncodingUTF8 ); 
    char *cStr = NewPtr( 1 + maxBytesNeeded ); 

    if ( cStr ) 
    {
        if ( !CFStringGetCString( inString, cStr, len, kCFStringEncodingUTF8 ) ) 
        {
            DisposePtr( cStr ); 
            cStr = nil; 
        }

        if ( cStr ) 
            // 新しいサイズが古いサイズより小さい場合、SetPtrSize は Mac OS X では
            // なんの作用も及ぼしません。
            // もし本当にメモリサイズを小さくしたい場合、stllen( cStr) + 1 の大きさの
            // 新しいメモリを確保し、そこに cStr の内容をコピーする必要があります

            SetPtrSize( cStr, strlen( cStr ) + 1 ); 
    }

    return cStr; 
}

ファイル名はどのようにエンコードされているのか

この項の説明は Apple Computer, Inc. でフォントと Unicode 担当をしている
Deborah Goldsmith 氏による Carbon-Developmentメーリングリストへの投稿を元にしています

HFS+ ではファイル名は正規形D (NFD) を Apple が改変した形式の UTF-16 として保持しています。この形式は Unicode 分解とシンボルブロックの一部において、互換性を損ねる場合があるのですが、これは Mac OS エンコーディングのファイル名と相互変換を保証するためです。(HFS API 群を使っているアプリケーションは、入力したファイル名と出力されたファイル名が同一バイト列であることを仮定しているため、Mac OS 9 において後方互換性を保持するために必要だったのです)

Mac OS X 10.2 では、Unicode 2.0.x (正確には中間段階のドラフトを基にしていたのですが)に上述の Apple による改変を加えた分解ルールから、Unicode 3.2 + Apple 改変というルールに変更されました。Unicode コンソーシアムは Unicode 3.2 以降では分解ルールを変えないことを決めたので、今後はこうした変更は起きないものと思われます。2.0.x から 3.2 への変更は、以下の 2点において必要な措置だったのです。

  1. 新しい分解ルールが数多く付け加えられた
  2. 2.0.x のデータはエラーに満ちていた

他のファイルシステムは別の方式で保存しています。UFS は UTF-8 ですし、HFS は Mac OS エンコーディング、AFP(AppleShare) の場合 3.0以前は Mac OS エンコーディングで、それ以降なら UTF-16 になっています。


(C) 2003 SkyTag Software, Inc.
訳文に対してのフィードバックは amn@mugiwara.jp まで

[index]