NSURL と Security-scoped bookmark の挙動の詳細

本稿では security-scoped bookmark に関する API の詳細な挙動について解説する。挙動は Mac OS X 10.8 当時のもの。（補足：投稿日時は2019年ですが、この記事は2013年に書かれました。内容は当時の実装を参考にしているため現在とは挙動が異なる可能性があります。）

# 前提知識
## Bookmark とは
Bookmark とは、ファイルやフォルダなどのファイルシステムリソース（以下「リソース」）の実体を指し示すデータである。あるリソースを指す NSURL オブジェクトから、そのリソースを指す bookmark を生成することができる。 Bookmark を _解決_ すると、同じリソースを指す NSURL オブジェクトを取得できる。

Bookmark はリソースがリネームされてもリソースの実体を参照し続ける。つまり、あるファイル `file:///path/to/file.txt` から bookmark を生成したのち、そのファイルを `file:///new_path/to/new_file.txt` に移動し、 bookmark を解決すると、 **移動後の** URL を取得できる。

## Security-scoped bookmark とは
Security-scoped bookmark とは、アプリケーションサンドボックスに対応した bookmark である。

Mac App Store で配布されるアプリケーションはサンドボックス内で実行されるため、サンドボックス外のリソースにアクセスすることができない。ただし、以下に該当するリソースはサンドボックス内に取り込まれ、アクセスが可能となる：

- アプリケーションパッケージ以下（`Foo.app/*`）にあるリソース
- コンテナ以下（`~/Library/Containers/com.example.Foo/*`）にあるリソース
- アプケーションの Entitlements.plist でアクセス許可を要求したリソース（`~/Documents` など）
- アプリケーション実行中に、ユーザーが NSOpenPanel などを操作して開いたリソース
- 以上のいずれの条件も満たさないリソースのうち、そのリソースを指すセキュリティスコープ情報つきの URL をレシーバにして `-[NSURL startAccessingSecurityScopeResource]` を呼んだもの（ただし対応する `-[NSURL stopAccessing...]` が呼ばれる前に限る）

※条件を満たすリソースがフォルダである場合、そのフォルダ以下のリソースも再帰的にサンドボックスに取り込まれる。

上記のうち、ユーザーが NSOpenPanel などを操作して開いたリソースがサンドボックス内に含まれるのは、アプリケーションが終了するまでである。よって、終了時にそのリソースの URL を保存しておき、次回の実行時にリソースを読み込むといったことができない。

Security-scoped bookmark は、リソースがサンドボックス内に取り込まれたという付加情報（以下「セキュリティスコープ情報」）を保持することで、アプリケーションの実行をまたいだリソースアクセスを可能にする。

サンドボックス内のリソースを指す NSURL オブジェクトから security-scoped bookmark を生成し、それを解決して得られる URL には、 `?applesecurityscope=...` というパラメータが付加される。この URL に対して `-[NSURL startAccessingSecurityScopeResource]` を呼ぶと、（実行をまたぐなどしてリソースがサンドボックス外に出ている場合でも）リソースがサンドボックスに取り込まれ、アクセスが可能になるのである。

補足：リソースがサンドボックス内に含まれる間は、そのリソースにアクセスするための URL にセキュリティスコープ情報がついている必要はない。セキュリティスコープ情報なしの URL やパス文字列を使ってもリソースにアクセスすることができる。

# 非サンドボックス化アプリケーションでの挙動
非サンドボックス化アプリケーションで、 security-scoped bookmark に関係した API を呼んだときの挙動について。

`-[NSURL startAccessingSecurityScopedResource]`、`-[NSURL stopAccessingSecurityScopedResource]`を呼んでも何も起こらない。

URL から security-scoped bookmark を生成することはできない。
`-[NSURL bookmarkDataWithOptions:includingResourceValuesForKeys:relativeToURL:error:]` に `NSURLBookmarkCreationWithSecurityScope` を渡すと nil が返り、エラーがセットされる：`Error Domain=NSCocoaErrorDomain Code=256 "The file “...” couldn’t be opened."`

普通の bookmark を security-scoped bookmark として解決することはできない。`+[NSURL bookmarkDataWithOptions:includingResourceValuesForKeys:relativeToURL:error:]` に `NSURLBookmarkResolutionWithSecurityScope` を渡すと nil が返り、 **エラーはセットされない** 。[サンドボックス化アプリケーションの挙動](#4-2)と異なることに注意。

よって、サンドボックス外では、 bookmark を生成・解決するメソッドに security scope 関係のフラグを渡さないように注意する。リソースへのアクセス時にはサンドボックス化アプリケーションと同様に start/stopAccessing... を呼んで構わない。

# サンドボックス化アプリケーションでの挙動
サンドボックス化アプリケーションで security-scoped bookmark や普通の bookmark を生成・解決する挙動について。

- サンドボックス内のリソースを指す URL から普通の bookmark を生成することができる。
- サンドボックス内のリソースを指す URL から security-scoped bookmark を生成することができる。
- サンドボックス外のリソースを指す URL から普通の bookmark を生成することはできない。
- サンドボックス外のリソースを指す URL から security-scoped bookmark を生成することはできない。
- Security-scoped bookmark を解決してセキュリティスコープ情報なしの URL を取得することができる。
- Security-scoped bookmark を解決してセキュリティスコープ情報つきの URL を取得することができる（URL に `?applesecurityscope=...` とパラメータが付加される）。
- 普通の bookmark を解決してセキュリティスコープ情報なしの URL を取得することがきる。
- 普通の bookmark を解決してセキュリティスコープ情報つきの URL を取得することはできない。

## リソースアクセスに関する各種エラーについて
### サンドボックス外のリソースを指す URL から普通の bookmark または security-scoped bookmark を生成しようとしたとき
`Error Domain=NSCocoaErrorDomain Code=256 "The file “...” couldn’t be opened."`

### 普通の bookmark を解決してセキュリティスコープつきの URL を取得しようとしたとき
`Error Domain=NSCocoaErrorDomain Code=259 "The file couldn’t be opened because it isn’t in the correct format."`

### サンドボックス外のリソースにアクセスしたとき
`Error Domain=NSCocoaErrorDomain Code=257 "The file “...” couldn’t be opened because you don’t have permission to view it."`