iOSの写真アプリの画像や動画のPHAssetがどのアプリから保存されたのかを調べる

# iOSの写真アプリの画像や動画のPHAssetがどのアプリから保存されたのかを調べる

Photos Frameworkを利用して、写真アプリに保存された画像や動画のPHAssetを取得し、iOSの写真アプリのように画像をリスト表示する機能を作る事がよくあります
自作した画像リストでも、iOSの写真アプリにある「〇〇から保存」という表示を実現できないか調べてみました
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/5aed8c02-20e3-3c9f-b470-2e2f7fe3c47b.jpeg" width="300">
iOS 15.1で動作確認しています
  (手元で動作確認しただけで、互換性など含めて諸々調べきれていないため、実際に使う場合はご注意ください🙇)

# xattr を使って保存したアプリを調べる
Photos Framework には、PHAssetを保存したアプリの情報を提供する機能は見当たりませんでした
しかし、保存されたファイルのExtended Attributesの `com.apple.assetsd.creatorBundleID`に保存したアプリのBundle IDが設定されていました
そこで、xattrを使って、Extended Attributesを取得して、保存したアプリを調べます

## xattr を使って Extended Attributes を取得する
`listxattr`と`getxattr`をSwiftから使いやすいようにWrapperを作成しました

```swift
public struct XattrError: Error {
    private let str: String? = String(utf8String: strerror(errno))
    public var localizedDescription: String { str ?? "unknown" }
}

public enum xattr {
    public static func list(from url: URL) throws -> [String] {
        let path = url.path
        let len = listxattr(path, nil, 0, 0)
        guard len > 0 else { throw XattrError() }

        let namebuff = UnsafeMutablePointer<CChar>.allocate(capacity: len)
        defer { namebuff.deallocate() }
        let res = listxattr(path, namebuff, len, 0)
        guard res > 0 else { throw XattrError() }

        guard let str = NSString(bytes: namebuff, length: len, encoding: String.Encoding.utf8.rawValue) else {
            throw XattrError()
        }
        var strs = str.components(separatedBy: "\0")
        strs.removeLast()
        return strs
    }

    public static func get(from url: URL, name: String) throws -> Data {
        let path = url.path
        let len = getxattr(path, name, nil, 0, 0, 0)
        guard len > 0 else { throw XattrError() }

        let value = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: MemoryLayout<UnsafeMutableRawPointer.Pointee>.alignment)
        defer { value.deallocate() }
        let res = getxattr(path, name, value, len, 0, 0)
        guard res > 0 else { throw XattrError() }

        let data = Data(bytes: value, count: len)
        return data
    }
}
```

この xattrを使って、指定したURLのファイルのExtended Attributesに　`com.apple.assetsd.creatorBundleID`が含まれていた場合は、Dataを取得して文字列として表示します

```swift
let url: URL

let list = try! xattr.list(from: url)

guard list.contains("com.apple.assetsd.creatorBundleID") else { return }

let creatorBundleID = try! xattr.get(from: url, name: "com.apple.assetsd.creatorBundleID")
print("creatorBundleID: \(String(data: creatorBundleID, encoding: .utf8)!)")
```

## PHAsset のファイルの URL を取得する

前項のxattrはファイルのURLを対象としているため、PHAssetのファイルのURLを取得します
適当にファイルのURLを取得して、Extended Attributesを表示させます

```swift
let options = PHFetchOptions()
options.sortDescriptors = [.init(key: "creationDate", ascending: false)]
let assets = PHAsset.fetchAssets(with: options)
assets.enumerateObjects { (obj: PHAsset, idx: Int, stopPtr: UnsafeMutablePointer<ObjCBool>) in
    getUrl(asset: obj) { (url: URL) in
        print("obj: \(obj)")
        print("url: \(url)")
        
        let list = try! xattr.list(from: url)
        print("list: \(list)")
        
        guard list.contains("com.apple.assetsd.creatorBundleID") else { return }
        
        let creatorBundleID = try! xattr.get(from: url, name: "com.apple.assetsd.creatorBundleID")
        print("creatorBundleID: \(String(data: creatorBundleID, encoding: .utf8)!)")
    }
}

func getUrl(asset: PHAsset, completion: @escaping (URL) -> Void) {
    switch asset.mediaType {
    case .image:
        let options = PHContentEditingInputRequestOptions()
        asset.requestContentEditingInput(with: options) { (contentEditingInput: PHContentEditingInput?, info: [AnyHashable : Any]) in
            completion(contentEditingInput!.fullSizeImageURL!)
        }
    case .video:
        let options = PHVideoRequestOptions()
        PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) in
            let urlAsset = asset as! AVURLAsset
            completion(urlAsset.url)
        }
    default:
        break
    }
}
```

## 結果

PHAssetのURLとExtended Attributesのリストと、`com.apple.assetsd.creatorBundleID`のプロパティを取得できました

```swift
url: file:///var/mobile/Media/DCIM/XXXAPPLE/IMG_XXX.MP4
list: ["com.apple.assetsd.UUID", "com.apple.assetsd.addedDate", "com.apple.assetsd.assetType", "com.apple.assetsd.avalanche.type", "com.apple.assetsd.cloudAsset.UUID", "com.apple.assetsd.creatorBundleID", "com.apple.assetsd.customCreationDate", "com.apple.assetsd.dbRebuildUuid", "com.apple.assetsd.deferredProcessing", "com.apple.assetsd.favorite", "com.apple.assetsd.hidden", "com.apple.assetsd.importedBy", "com.apple.assetsd.importedByDisplayName", "com.apple.assetsd.originalFilename", "com.apple.assetsd.publicGlobalUUID", "com.apple.assetsd.sceneAnalysisIsFromPreivew", "com.apple.assetsd.syndicationHistory", "com.apple.assetsd.timeZoneName", "com.apple.assetsd.timeZoneOffset", "com.apple.assetsd.trashed", "com.apple.assetsd.videoComplementVisibility"]
creatorBundleID: hoge.fuga.piyo.bundle.identifier
```

# まとめ
iOSの写真アプリの画像や動画のPHAssetがどのアプリから保存されたのかを調べる方法を調査してみました
最初は、[Photos.sqlite を直接読み込む](https://theforensicscooter.com/2021/11/23/photos-sqlite-queries/)のも検討していましたが、結果的に比較的シンプルに画像を保存したアプリを調べる事ができました
最初にも書きましたが、互換性など含めて諸々調べきれていないので、実際に使う場合はご注意頂けると幸いです