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
}
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.
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.
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.
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.
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.