More Fun Scripting with Swift in Xcode: extended attributes

Like many scripting projects, xattred started off very modestly, as a convenient way to inspect extended attributes without toil at the command line. Then it sort of grew: first to try to add a quarantine xattr, and now it’s threatening to copy, paste, and manipulate xattrs in all sorts of other ways too.

xattred2

Viewing any extended attributes is a matter of calling xattr -l pathname. To do this, I use File Open sheet to deliver the URL of the file or folder in question, then wrap that into the arguments of a call to the xattr command. The result returned from that is then turned into an attributed string, and displayed.

xattred8

The only slightly unusual line of code here is
(self.view.window?.windowController?.document as! Document).displayName = self.theFilename + "_MD"
which appends _MD to the end of the file/folder name, and makes that the display name for that window, which in turn becomes the default name to use for saving the xattrs as text. The self references are needed because this is set within a closure.

xattred5

Adding a quarantine xattr to the selected file or folder requires a sequence of three commands: the first obtains a UUID using uuidgen; the second writes the complete xattr to the chosen item using xattr; the third enters details of the event, with its time and UUID, into the SQLite database stored in ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2.

This is what happens in outline. First I get the UUID:
let task = Process()
task.launchPath = "/usr/bin/uuidgen"
task.arguments = []
task.launch()
task.waitUntilExit()
let status = task.terminationStatus

This can be safely replaced with the direct Foundation call
let theUUID = UUID.init()
which obtains a new and unique UUID, which can then be accessed as a string, as in
let theStr = theUUID.uuidString

If the status is 0 (success), I form the arguments for xattr:
theTimeStr = String(format:"%2X", Int(NSDate().timeIntervalSince1970))
theUUIDStr1 = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
theUUIDStr = theUUIDStr1.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let theBigStr = "0083;" + theTimeStr + ";xattred.app;" + theUUIDStr
var theCmdArgs = ["-w", "com.apple.quarantine", theBigStr]

Using the Foundation call to get a UUID will spare me from having to trim its trailing newline character as shown above.

NSOpenPanel is used again to obtain the full path of the item to which the xattr will be appended, which is added as the final argument before building another task and running it. If that also returns a status of 0, the last step is to assemble the SQL command:
let thePath2DB1 = FileManager.default.homeDirectoryForCurrentUser.path as String
let theDateStr = String(Int(NSDate().timeIntervalSinceReferenceDate))
let thePath2DB2 = thePath2DB1 + "/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2"
let theSQLCmd = "INSERT INTO \"LSQuarantineEvent\" VALUES('" + theUUIDStr + "'," + theDateStr + ",NULL,'xattred.app','https://example.com/this/that.zip',NULL,NULL,0,NULL,'https://example.com',NULL);"
let theCmdArgs2 = [thePath2DB2, theSQLCmd]
let task2 = Process()
task2.launchPath = "/usr/bin/sqlite3"
task2.arguments = theCmdArgs2

This is fine for what is little more than macOS scripting, but not good form for a real app. Calling shell commands all the time is not efficient, and would be much better performed directly. The problem in working with xattrs lies in there being no Objective-C or Swift wrappers to the C function calls which manipulate them. Apple has failed to provide more convenient interfaces equivalent to, or even within, FileManager.

Martin R has, on StackOverflow, provided free source to extend URL and NSURL with wrappers for some of the xattr functions. For example, his code to write a xattr from NSURL is:
func setExtendedAttribute(data: [UInt8], forName name: String) -> Bool {
// Get local file system path:
var fileSystemPath = [Int8](count: Int(MAXPATHLEN), repeatedValue: 0)
guard self.getFileSystemRepresentation(&fileSystemPath, maxLength: fileSystemPath.count) else {
return false
}
// Set attribute:
let result = data.withUnsafeBufferPointer {
setxattr(fileSystemPath, name, $0.baseAddress, data.count, 0, 0)
}
return result == 0
}

There is more work to be done. For example, the C call to obtain a list of xattrs for a path to file/folder is
ssize_t listxattr(const char *path, char *namebuf, size_t size, int options)
which returns the size of the list of extended attribute names, and those names in namebuf. It has to be called twice, though: the first call, with namebuff set to NULL, is used to obtain the required size of namebuf for the second call, which actually retrieves those names.

Before xattred can do anything more serious with xattrs, I’m going to need decent Swift wrappers to work directly with them, through that C interface, rather than having to keep calling shell commands. That takes me beyond simple scripting, but perhaps to a more generally useful tool.