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.