Search code examples
javascriptmacosterminalnscoder

How can I decode an `NSCoder` with a terminal command


This is a continuation of this question I asked on Ask Different.

I am trying get the list of recently opened documents in Pages. I found the file at ~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.apple.iwork.pages.sfl2 and successfully used plutil -convert xml1 to convert it to an xml plist file. Thanks to @Graham Miln I know that the file is encoded with an NSCoder.

I know absolutely nothing about Objective-C and am looking for a way to decode the file within terminal. I am decoding this file for a raycast extension which will be written in JavaScript. Therefore, I am either looking for a JavaScript solution or a terminal command that I can run(because it is really easy to run terminal commands in the js code).


Solution

  • Below is a Swift script that decodes any sfl12 file, and prints each recent file, one file per line.

    Save it and either execute with with the swift /path/to/saved/file </path/to/sfl12/file>, or make the file executable and directly execute it.

    The script expects the path to the sfl12 file as first argument.

    Note that you need to install the Xcode Command Line Tools in order to be able to execute swift scripts.

    #!/usr/bin/swift
    import Foundation
    
    do {
        guard CommandLine.arguments.count > 1 else {
            throw "Please provide the path to the file"
        }
    
        let data = try Data(contentsOf: URL(filePath: CommandLine.arguments[1]))
        guard let root = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSArray.self], 
                                                                from: data) as? [String: Any] else {
            throw "Invalid top level object, expected a dictionary"
        }
    
        guard let items = root["items"] as? [[String: Any]] else {
            throw "Invalid items property, expected an array of dictionaries"
        }
    
        for item in items {
            var stale = false
            if let bookmarkData = item["Bookmark"] as? Data,
               let url = try? URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &stale) {
                    print(url.path)
            }
        }
    } catch {
        print(error.localizedDescription)
        exit(-1)
    }
    
    extension String: LocalizedError {
        public var errorDescription: String? { self }
    }
    

    Sample output:

    > swift ~/Downloads/recent-files.swift "/Users/<myusername>/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.apple.iwork.pages.sfl2"
    
    /Users/<myusername>/Documents/test2.pages
    /Users/<myusername>/Desktop/test.pages