Ad

Our DNA is written in Swift
Jump

Reading and Writing Extended File Attributes

The file systems of iOS and Mac both use HFS+ as file system, with only one small difference. iOS uses case-sensitive file names, Mac doesn’t by default. Both have a feature called “Extended File Attributes” that allow you to set custom values by key.

Apple generally ignored this functionality and it was only briefly – in 5.0.1 – that they actually used an extended attributed for something. The “com.apple.MobileBackup” extended attribute served as a stop-gap-measure to mark files that should not be backed up. Though this was very short lived.

I was facing the problem myself that I needed to save the ETag for an image that I downloaded from the web somewhere. And I wanted to do that elegantly, somehow together with the file itself and in a way that would simply work.

Well, the best way for achieving this simple goal was to save the ETag value in the extended file attributes. This way you don’t have to keep track of a separate file or storage for the value. If the file is deleted, then that takes care of the extended attributes as well.

Extended attributes are managed with C-API defined in sys/xattr.h, so most of the work in translating to and from Objective-C is converting strings.

To set an attribute you do this:

const char *attrName = [attribute UTF8String];
const char *filePath = [_path fileSystemRepresentation];
 
const char *val = [value UTF8String];
 
int result = setxattr(filePath, attrName, val, strlen(val), 0, 0);

Note that result has to be a signed integer because it returns -1 in case of error.

The above assumes that you want to set a string as value which is the most versatile. But in fact you can set any kind of byte data because you specify a pointer to the first byte and then number of bytes to write. So you can also set an integer like this:

int val=1;
int result = setxattr(filePath, attrName, &val, sizeof(val), 0, 0);

In my special case I was content with setting an NSString. I didn’t see any way how you could know from the data from what bytes it was actually set. I guess you have to make your own rules and only access/decode your own extended attributes.

To retrieve the extended attribute value you do this:

const char *attrName = [attribute UTF8String];
const char *filePath = [_path fileSystemRepresentation];
 
// get size of needed buffer
int bufferLength = getxattr(filePath, attrName, NULL, 0, 0, 0);
 
// make a buffer of sufficient length
char *buffer = malloc(bufferLength);
 
// now actually get the attribute string
getxattr(filePath, attrName, buffer, 255, 0, 0);
 
// convert to NSString
NSString *retString = [[NSString alloc] initWithBytes:buffer length:bufferLength encoding:NSUTF8StringEncoding];
 
// release buffer
free(buffer);

Here you see getxattr actually being called twice. If you don’t set a pointer to a buffer this retrieves the size of the value in bytes for the attribute. So you can malloc a sufficiently large buffer and then get this filled to the brim on the second call.

All commands in xattr.h have two variants, one with a file path and a second with with a file descriptor. The latter would be used in cases where you want to keep the file descriptor (sort of a handle to the file) around doing more work. But we can ignore these.

There are also two more functions, for sake of completeness, one to remove an extended attribute, one to get a list of all keys. I contemplated for a while to make an API similar to NSFileManager, but since we don’t know the data type (unless we save that somewhere) there is little use to build a method that sets or retrieves and entire dictionary of keys and values. Also, for now, I only need to save and retrieve a single string.

I created a documented wrapper for handling extended file attributes in DTFoundation and I named it DTExtendedFileAttributes.


Categories: Recipes

3 Comments »

  1. In your code snippet above, you pass 255 as the buffer size in the second call. You are going to truncate any values that are longer than that.

    You should be passing bufferLength.

  2. It’s a very bad idea to directly store an integer as described. If Apple ever chooses to support a big endian architecture (like PPC) again, you will get into a world of pain.

    Apple themselves appear to only ever store strings or (binary) plists. Even number values are stored as strings.

  3. #import