Beyond Scripting in Swift: Direct access to xattrs, calling C, and converting Data to Strings

The initial version of my tool xattred for viewing and manipulating extended attributes, xattrs, was a quick and dirty scripting solution. It relied on a series of calls to command tools such as xattr and uuidgen which are costly, and needed to be replaced by direct calls to macOS.

The problem with those direct calls is that Apple provides only a C interface to them. For example, the function to get a xattr value is documented as having the interface
ssize_t getxattr(const char *path, const char *name, void *value, size_t size, u_int32_t position, int options)
where ssize_t is the size of the xattr data or -1 to indicate an error, path is a null-terminated UTF-8 string giving the full path to the file/folder, name is a null-terminated UTF-8 string containing the name of the xattr, value is a pre-allocated buffer to contain the xattr value, size is the maximum size of the data to be returned, position sets any offset within the xattr at which to start reading it, and options sets a couple of options.

You have to call this twice: the first time with value set to NULL doesn’t return any data, but the result allows you to pre-allocate a buffer of the correct size for the second call, which actually gets the xattr data.

Fortunately Martin R has already provided an excellent Swift wrapper to these C calls, as extensions to URL and NSURL. I decided to use the URL extensions, which in the case of getxattr() are coded as:
func extendedAttribute(forName name: String) throws -> Data {
let data = try self.withUnsafeFileSystemRepresentation { fileSystemPath -> Data in
let length = getxattr(fileSystemPath, name, nil, 0, 0, 0)
guard length >= 0 else { throw URL.posixError(errno) }
var data = Data(count: length)
let result = data.withUnsafeMutableBytes {
getxattr(fileSystemPath, name, $0, data.count, 0, 0)
}
guard result >= 0 else { throw URL.posixError(errno) }
return data
}
return data
}

xattred20

Martin’s wrapper provides the following functions, each of which throws errors which can be interpreted into an NSError using
private static func posixError(_ err: Int32) -> NSError

func extendedAttribute(forName name: String) throws -> Data
func setExtendedAttribute(data: Data, forName name: String) throws
func removeExtendedAttribute(forName name: String) throws
func listExtendedAttributes() throws -> [String]

These make coding the actions for each of xattred’s button controls very simple. For example, that to remove a xattr displays the File Open panel as a sheet, for the user to select the file or folder. The xattr name is then fetched from its text box, and provided that it is not empty, the function removeExtendedAttribute() is called in a try:
do {
try theSourceURL.removeExtendedAttribute(forName: theXattrName)
self.filenameTextbox.stringValue = theSourcePath
} catch let error {
doErrorAlertModal(message: ("removexattr error: \(error.localizedDescription)"))
}

If the call flags an error, an English description is then displayed in an error alert.

xattred21

The most complex action is that for the Inspect button. After handling the selection of the file/folder to be inspected, a string array containing the list of xattrs is obtained using listExtendedAttributes(). That array is iterated through to obtain each of the xattrs using the call extendedAttribute(), which returns the contents of the named xattr as Data.

xattred22

One disadvantage in performing this using direct calls is that the command tool xattr returns xattrs neatly formatted in hex with text equivalents. Rolling your own equivalent for Data is non-trivial, because you cannot just force the conversion of Data to UTF-8, for instance.

xattred2

Some xattrs, notably the quarantine flag, are pure UTF-8 text, and will convert neatly using the String initialisation
String.init(data: data1, encoding: .utf8)

But many xattrs contain binary, and that call will then return nil. Currently xattred converts those to a hex listing using NSData.description, as Data.description merely returns the size of the data, then displays an ASCII text conversion using String.init(data: data1, encoding: .ascii), which allows the user to see any embedded text. I haven’t yet found a better generic Data to String conversion which won’t return nil when fed purely binary data.

xattred13

Now that xattred has a more useful and functional engine for working with xattrs, it’s time to look seriously at giving it a more friendly and powerful interface.

Another interesting issue which I need to think about more carefully is Undo and protecting the user from their mistakes: I have been surprised to discover that writing a xattr to a file/folder which already has a xattr of the same name overwrites the old one without warning. macOS still handles xattrs in a very basic way.