[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 から転送

```Objective-C:ファイルの存在チェック
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] を使うようにする

```Objective-C:
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 を使う

```Objective-C: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 を使うべし、という記述を見かける（[ここ](http://skyarts.com/blog/jp/skyarts/?p=31678&cpage=1)、[ここ](http://qiita.com/nagisawks/items/a6268a1836d867e35324)など）が、NSString.fileSystemRepresentation だとアクセス出来ない。何かが間違えているのだろうか？

```Objective-C: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](https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/APFS_Guide/FAQ/FAQ.html#//apple_ref/doc/uid/TP40016999-CH6-DontLinkElementID_3)によれば、「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 などの標準出力文字列をコピーした際に、正規化形式もそのままコピペできる 

```Objective-C:NFC/NFD文字列をコピペ
NSString* filename1 = @"ぱ_Win.pdf"; // NFC
NSString* filename2 = @"ぱ_Win.pdf"; // NFD

NSLog(@"isEqual? %d", [filename1 isEqualToString:filename2]);
```

```:出力結果
isEqual? 0
```

* Emacs では、NFCとNFD文字列を視覚的に判別できる
![スクリーンショット 2017-04-09 17.18.19.png](https://qiita-image-store.s3.amazonaws.com/0/53928/15ab9c7a-75bd-69a9-2abf-9cb113a7fc8d.png)


# 参考
## iOS10.3 / APFS
* [iOS10.3のファイルパス問題について](http://skyarts.com/blog/jp/skyarts/?p=31678&cpage=1)
* [iOS10.3 APFSの対処 - Qiita](http://qiita.com/nagisawks/items/a6268a1836d867e35324)
* [macOS上のAPFSはUnicode Normalizationを行うのか?](http://naruse.hateblo.jp/entry/2017/03/28/181519)
* [Mac OS X の NFD 問題での対策諸々 - Qiita](http://qiita.com/knaka/items/48e1799b56d520af6a09)
* [Swiftでの文字列比較におけるUnicode正規化を巡る注意点 - Qiita](http://qiita.com/doraTeX/items/4feea8d9221419b19954)
* [SwiftでのUnicode正規化問題 続編：HFS+との整合性 - Qiita](http://qiita.com/doraTeX/items/219d329e8a773e715631)
* [Apple File System Guide](https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/APFS_Guide/FAQ/FAQ.html#//apple_ref/doc/uid/TP40016999-CH6-DontLinkElementID_3)
* [2byte language problem in iOS 10.3 | Apple Developer Forums](https://forums.developer.apple.com/thread/73571)
* [APFS’s “Bag of Bytes” Filenames](https://mjtsai.com/blog/2017/03/24/apfss-bag-of-bytes-filenames/)
* [APFS does not normalize Unicode filenames | Hacker News](https://news.ycombinator.com/item?id=13953800)
* [HFS+のテキストエンコーディング](http://tama-san.com/hfsplus/)

## Unicode 正規化形式
* [Unicode正規化 - Wikipedia](https://ja.wikipedia.org/wiki/Unicode%E6%AD%A3%E8%A6%8F%E5%8C%96)
* [[iOS] 同じようにみえる文字が、[NSString isEqualToString:] で false になってしまう](http://qiita.com/plusadd/items/c2ec74eb2e19d068a421)

## API
* [initFileURLWithFileSystemRepresentation:isDirectory:relativeToURL: - NSURL | Apple Developer Documentation](https://developer.apple.com/reference/foundation/nsurl/1411210-initfileurlwithfilesystemreprese)