[iOS] iOS10.3 で、ファイル名に濁点・半濁点のついたファイルにアクセスできない(Unicode 正規化)


  • 追記(2017/04/09 23:50) Windows が採用しているファイルパスの正規化形式「NFC」で作成されたファイルにアクセス出来ないことが問題なのであり、「iTunes for Windows」の問題ではないと考えられる。そこでタイトルを以下のように変更。
    • 旧:[iOS] iOS10.3 で、iTunes for Windows から転送した濁点付きファイルにアクセスできない(Unicode 正規化)
    • 新:[iOS] iOS10.3 で、ファイル名に濁点・半濁点のついたファイルにアクセスできない(Unicode 正規化)

はじめに

  • iOS10.3上のアプリが、Windows などで作成した濁点・半濁点付きのファイルにアクセスできない問題について、解決方法を書いたものである。
  • コメント、ツッコミ歓迎です。

問題

  • Windows 上で作成したファイルを、iTunes のファイル共有機能などを通じてアプリに転送した際、濁点・半濁点を含むファイルにアプリ上からアクセスできない
  • 環境
OS/App Version
iOS 10.3.1
Windows 7
iTunes 12.6
Xcode 8.3
  • 例:2つのファイルにアクセス
    • ぱ_Mac.pdf:macOS(Sierra) の iTunes から転送
    • ぱ_Win.pdf:Windows7 の iTunes から転送
ファイルの存在チェック
NSArray* filepaths = ...
NSFileManager *fileManager = [NSFileManager defaultManager];

for(NSString *filepath in filepaths) {

    NSLog(@"==== %@ ====", filepath.lastPathComponent);

    NSLog(@"NSFileManager\t=> %@", ([fileManager fileExistsAtPath:filepath] ? @"YES":@"NO"));
}
出力結果(iOS10.0.1)
==== ぱ_Mac.pdf ====
NSFileManager   => YES

==== ぱ_Win.pdf ====
NSFileManager   => YES
出力結果(iOS10.3)
==== ぱ_Mac.pdf ====
NSFileManager   => YES

==== ぱ_Win.pdf ====
NSFileManager   => NO <<<< ??

原因

  • 実際のファイル名と、アクセスする際のファイル名について、それぞれの Unicode の正規化形式が異なり、別ファイル扱いとなってしまうため
アクセス方法 正規化形式 備考
Windows で作成したファイル NFC macOS で作成したファイルは NFD
NSFileManagerでアクセス NFD iOS10.3 で採用されたファイルシステム Apple File System(APFS) は、 NFD(Normalization Form Canonical Decomposition) 形式を前提としており、NSFileManager が内部でNFD形式に変換してるものと考えられる

解決方法

Foundation API を使う場合

  • [[NSURL alloc] initFileURLWithFileSystemRepresentation:[filepath UTF8String] isDirectory:NO relativeToURL:nil] を使うようにする
for(NSString* filepath in filepaths) {

     NSLog(@"==== %@ ====", filepath.lastPathComponent);

     NSURL* fileURL = [[NSURL alloc] initFileURLWithFileSystemRepresentation:[filepath UTF8String] isDirectory:NO relativeToURL:nil];
     NSLog(@"exist(NSURL)\t=> %@",     ([fileURL checkResourceIsReachableAndReturnError:nil] ? @"YES":@"NO"));
}
出力結果(iOS10.0.1)
==== ぱ_Mac.pdf ====
NSURL  => YES

==== ぱ_Win.pdf ====
NSURL  => YES
出力結果(iOS10.3)
==== ぱ_Mac.pdf ====
NSURL  => YES

==== ぱ_Win.pdf ====
NSURL  => YES

fopen などの低レベルな関数を使う場合

  • NSURL.fileSystemRepresentation か、 NSString.UTF8String を使う
fopen
...
for(NSString* filepath in filepaths) {

    NSLog(@"==== %@ ====", filepath.lastPathComponent);

    // NSURLでファイルパス情報を取得できたと仮定
    NSURL* fileURL = [[NSURL alloc] initFileURLWithFileSystemRepresentation:[filepath UTF8String] isDirectory:NO relativeToURL:nil];

    NSLog(@"fopen(NSRUL.fileSystemRepresentation)\t=> %@", ((fopen([fileURL fileSystemRepresentation], "r") != NULL) ? @"YES":@"NO"));
    NSLog(@"fopen(NSString.UTF8String)\t\t\t\t=> %@",      ((fopen([filepath UTF8String], "r") != NULL) ? @"YES":@"NO"));
}
出力結果(iOS10.0.1)
==== ぱ_Mac.pdf ====
fopen(NSURL.fileSystemRepresentation)   => YES
fopen(NSString.UTF8String)              => YES

==== ぱ_Win.pdf ====
fopen(NSURL.fileSystemRepresentation)   => YES
fopen(NSString.UTF8String)              => YES
出力結果(iOS10.3)
==== ぱ_Mac.pdf ====
fopen(NSURL.fileSystemRepresentation)   => YES
fopen(NSString.UTF8String)              => YES

==== ぱ_Win.pdf ====
fopen(NSURL.fileSystemRepresentation)   => YES
fopen(NSString.UTF8String)              => YES

疑問

NSString.fileSystemRepresentation の使用について

  • 低レベルな関数を使ってアクセスする場合、UTF8String ではなく NSString.fileSystemRepresentation を使うべし、という記述を見かける(ここここなど)が、NSString.fileSystemRepresentation だとアクセス出来ない。何かが間違えているのだろうか?
NSString.fileSystemRepresentation
...
for(NSString* filepath in filepaths) {

    NSLog(@"==== %@ ====", filepath.lastPathComponent);

    NSLog(@"fopen(fileSystemRepresentation)=> %@", ((fopen([filepath fileSystemRepresentation], "r") != NULL) ? @"YES":@"NO"));

}
出力結果
==== ぱ_Mac.pdf ====
fopen(fileSystemRepresentation) => YES

==== ぱ_Win.pdf ====
fopen(fileSystemRepresentation) => NO <<< ??
  • 追記(2017/04/09 23:10)
    • Apple File System Guide の FAQによれば、「Use the fileSystemRepresentation property of NSURL objects」とあるので、NString.fileSystemRepresentationではなく、NSURL.fileSystemRepresentation が正しいのではないだろうか。
    • 以下、引用(太線は追加)

To avoid introducing bugs in your code with mismatched Unicode normalization in filenames:
Use high-level Foundation APIs such as NSFileManager and NSURL when interacting with the filesystem
Use the fileSystemRepresentation property of NSURL objects when creating and opening files with lower-level filesystem APIs such as POSIX open(2), or when storing filenames externally from the filesystem

おまけ

  • Xcode などの標準出力文字列をコピーした際に、正規化形式もそのままコピペできる
NFC/NFD文字列をコピペ
NSString* filename1 = @"ぱ_Win.pdf"; // NFC
NSString* filename2 = @"ぱ_Win.pdf"; // NFD

NSLog(@"isEqual? %d", [filename1 isEqualToString:filename2]);
出力結果
isEqual? 0
  • Emacs では、NFCとNFD文字列を視覚的に判別できる

参考

iOS10.3 / APFS

Unicode 正規化形式

API