Please note: this article is part of the older "Objective-C era" on Cocoa with Love. I don't keep these articles up-to-date; please be wary of broken code or potentially out-of-date information. Read "A new era for Cocoa with Love" for more.
Resolving symlinks in a path is very easy in Cocoa (it can be done in a single statement) but aliases require more work. Additionally the commands for resolving symlinks and aliases are incompatible with each other — meaning that you can resolve a path containing symlinks or aliases but not a mixture of the two. In this post, I present a category on NSString
that will allow you to resolve a path containing any combination of symlinks or aliases as simply as resolving symlinks alone.
Introduction
When Apple introduced alias files in System 7, they were the only way of referencing another file in the filesystem. Sure, other operating systems already had symlinks but there was little likelihood of the two needing to interact since System 7 had almost no interoperability with Unix filesystems (despite the existence of A/UX).
Alias files are a great way of referencing other files because they can continue to track their target if it moves. Aliases store both the path of their target and the inode plus volume identifier of their target. This means that they won't break if the file at their target path moves, unlike a symlink which will break if the target moves (since a symlink is just a path). In fact, symlinks will often break if the symlink file itself moves since symlinks are normally relative paths.
Aliases do have some annoying traits. In their current incarnation, they insist on storing the thumbnail and metadata of data of their target, resulting in a file which may be hundreds of kilobytes (or more) of entirely redundant data — especially annoying if you have thousands of aliases. Despite this, they are normally a good, general-purpose way of linking files in the filesystem.
As a "user-exposed" feature (unlike symlinks which are largely hidden from novice users) they are also common. It's unfortunate then that they're so annoying to deal with in Cocoa.
Resolving aliases
Resolving a symlink is easy:
NSString *resolvedPath = [path stringByResolvingSymlinksInPath];
Theoretically, you only need one command to resolve an alias:
OSErr err = FSResolveAliasFile(
&fsRef, resolveAliasChains, &targetIsFolder, &wasAliased);
Unfortunately, the FSRef
here is a campy 1990's throwback (the 90's are now two decades ago) and in Cocoa programming, you rarely have or want an FSRef
for a file. This means that you need to convert your NSString
to an FSRef
and then convert the result back to an NSString
when you're done.
Apple's Low Level File Management Topics include an approach for resolving an alias from an NSString
path which demonstrates this procedure. While I use an implementation derived from this in my solution, Apple's original implementation suffers from the following problems:
- The
CFURLGetFSRef
function used to convert aCFURL
into anFSRef
will fail if the path contains an alias at anywhere other than the last path component. - While
CFURLGetFSRef
will follow symlinks in the URL to create theFSRef
, no part of this code will actually return a resolved symlink, so that part will require a separate step. - The function
FSResolveAliasFile
will present a user dialog if the alias points to a volume which is not mounted. While potentially desirable in a user application, this is undesirable in all other cases.
This final point is not too difficult — we'll replace FSResolveAliasFile
with FSResolveAliasFileWithMountFlags
which allows us to disable the user dialog using the flags. But the remaining two points will require a little more work to address.
As a further comment about usage of FSResolveAliasFileWithMountFlags
: aliases that point to other aliases are exceedingly rare (if you try to create an alias to an alias in the Finder, the Finder will make the second alias point directly to the target) so I pass false
for resolveAliasChains
to optimize for the unchained case and handle the unusual case of chained aliases at a different level in the code.
Breaking it down into solvable components
We can resolve paths that contain any number of symlinks and we can resolve a path that contains an alias but we can't do both at once.
The solution is therefore straightforward:
- break the path down into components
- build the components together, iteratively resolving aliases or symlinks at each level
- implement code for resolving the symlink or alias as efficiently as possible for the bottom level of this scenario
Each of these points will then be a different tier in a three level solution.
My solution will contain two requirements:
- The initial path must be resolvable to an absolute path using
-[NSString stringByStandardizingPath]
- The path contained within a symlink file will not include any aliases or symlinks except as the final string component (no recursive parsing)
This second point is mostly a theoretical limitation since it is nearly impossible to generate a symlink with an alias or other symlink as a non-final path component (you'd have to create the symlink file manually).
Top level
The top level of the solution is simply an iteration over path components which then invokes the iterative link resolution.
// Break into components.
NSArray *pathComponents = [path pathComponents];
// First component ("/") needs no resolution; we only need to handle subsequent
// components.
NSString *resolvedPath = [pathComponents objectAtIndex:0];
pathComponents =
[pathComponents subarrayWithRange:NSMakeRange(1, [pathComponents count] - 1)];
// Process all remaining components.
for (NSString *component in pathComponents)
{
resolvedPath = [resolvedPath stringByAppendingPathComponent:component];
resolvedPath = [resolvedPath stringByIterativelyResolvingSymlinkOrAlias];
if (!resolvedPath)
{
return nil;
}
}
I haven't shown the code which resolves the path to an absolute path or fails but the assumption that it begins with a "/" is valid.
Middle level
The middle level of the solution iterates over a path where only the final component could be a symlink or alias and resolves it until the result is neither an alias nor symlink.
For efficiency, this does two things in an unusual way:
- I use
lstat
instead of-[NSFileManager attributesOfItemAtPath:error]
since I only need thest_mode
field, andNSFileManager
invokeslstat
internally anyway. - I use my own
-[NSString stringByConditionallyResolvingSymlink]
method instead of- [NSString stringByResolvingSymlinksInPath]
since I know that only the final component requires resolution (I've already done the work for earlier components).
- (NSString *)stringByIterativelyResolvingSymlinkOrAlias
{
NSString *path = self;
NSString *aliasTarget = nil;
struct stat fileInfo;
// Use lstat to determine if the file is a directory or symlink
if (lstat([[NSFileManager defaultManager]
fileSystemRepresentationWithPath:path], &fileInfo) < 0)
{
return nil;
}
// While the file is a symlink or resolves as an alias, keep iterating.
while (S_ISLNK(fileInfo.st_mode) ||
(!S_ISDIR(fileInfo.st_mode) &&
(aliasTarget = [path stringByConditionallyResolvingAlias]) != nil))
{
if (S_ISLNK(fileInfo.st_mode))
{
// Resolve the symlink component in the path
NSString *symlinkPath = [path stringByConditionallyResolvingSymlink];
if (!symlinkPath)
{
return nil;
}
path = symlinkPath;
}
else
{
// Or use the resolved alias result
path = aliasTarget;
}
// Use lstat again to prepare for the next iteration
if (lstat([[NSFileManager defaultManager]
fileSystemRepresentationWithPath:path], &fileInfo) < 0)
{
path = nil;
continue;
}
}
return path;
}
The stringByConditionallyResolvingAlias
method returns nil
if the path exists but isn't an alias, allowing this function to double as both a test for whether the path is an alias as well as the resolution of that alias. I could use a similar approach to test and resolve symlinks (since I have also implemented a stringByConditionallyResolvingSymlink
method) but I don't do this for aforementioned efficiency reasons: it would cause an extra fetch of the filesystem metadata which is the main bottleneck of the whole procedure.
Bottom level
The bottom level is then just the implementation of the stringByConditionallyResolvingAlias
and stringByCondictionallyResolvingSymlink
. The first is just a modification of Apple's code to address the issues I've already discussed — you can see the final product by downloading the code. The second method looks like this:
- (NSString *)stringByConditionallyResolvingSymlink
{
// Get the path that the symlink points to
NSString *symlinkPath =
[[NSFileManager defaultManager]
destinationOfSymbolicLinkAtPath:self
error:NULL];
if (!symlinkPath)
{
return nil;
}
if (![symlinkPath hasPrefix:@"/"])
{
// For relative path symlinks (common case), resolve the relative
// components
symlinkPath =
[[self stringByDeletingLastPathComponent]
stringByAppendingPathComponent:symlinkPath];
symlinkPath = [symlinkPath stringByStandardizingPath];
}
return symlinkPath;
}
Hooray, I finally used NSFileManager
in a post about files! Yes, once again it's probably just a wrapper around the C function readlink
that I could invoke for myself but the fact that the NSFileManager
method handles the nasty business of buffer allocation, sizing and string conversion is more than enough reason to forego the lower level function.
Conclusion
You can download NSString+SymLinksAndAliases.zip (3kb) which contains all the code discussed in this post (plus a few other related methods) as a category on NSString
.
Usage of the category is as simple as importing the header and writing:
NSString *fullyResolvedPath = [somePath stringByResolvingSymlinksAndAliases];
The fullyResolvedPath
will either contain the destination (as an absolute and fully resolved path) or it will be nil
(if the path can't be fully resolved because it doesn't exist or can't be read for some reason).
I've tried to keep this code efficient by keeping the number of filesystem calls low. The code will certainly handle at least a few thousand alias resolutions per second on my computer but I haven't pushed it much harder than that.
Of course, if you're an iPhone programmer, all of this is a waste of time since the iPhone doesn't publicly expose FSRef
(although CFURLGetFSRef
exists to generate a pointer which is totally unusable). In fact, I'm not sure aliases are possible on the iPhone anyway.
The differences between Core Data and a Database