views:

501

answers:

4

This is on the Mac:

If I have two filenames /foo/foo and /foo/FOO they may refer to the same file or the may be different files depending on the file system. How do I figure out if they are both pointing to the same file? And if they are, how do I get the correct representation of the filename?

My problem is caused by links. A link might point to /foo/FOO but the actual directory is named /foo/foo.

Is there any function that will follow a link and give me the the full path of the linked file? [NSFileManager pathContentOfSymbolicLinkAtPath] gives relative paths that might be in the incorrect case.

Ultimately what I'm try to do is cache info for files. But if I have two different paths for the same file, my cache can get out of sync.

Thanks

A: 

AFAIK, by default the filesystem in Mac OS X is Case Insensitive, thus the case of the link or filename shouldn't matter.

Grant Limberg
My new iMac with Leopard gave me a choice the first time I turned it on. So it's entirely possible to have a factory-installed Mac with a case-sensitive file system.
Rob Kennedy
What the user gets by default is irrelevant. The user can have a case-sensitive file-system, either HFSX or UFS, on any device. Any application that does not handle this correctly is broken.
Peter Hosey
Add Photoshop CS3 to the list of "broken" applications. I'm still annoyed I had to reformat just to install it, and if it was any other application I would have just trashed the app instead.
Marc Charbonneau
Corollary to my earlier comment: Don't base your behavior on the boot file-system. The user may have other FSs mounted that are in other formats with different case behavior. Behave appropriately for each path.
Peter Hosey
+2  A: 

if os x is a unix derivative, do you have access to the inode #?

le dorfier
This would be the best idea. The stat(2) function should return a useful inode number. Note that the pair (device, inode) is the unique key, since different devices have their own sequences of inode numbers.
Greg Hewgill
That should work for the main filesystem. I don't know if it will work for a mounted NTFS or FAT32 drive.
FigBug
Then the "correct representation of the file name" is a non sequiter. File references to an inode are first-class filenames - there's no primary name.
le dorfier
+5  A: 

Use FSPathMakeRef() on both paths, and then use FSCompareFSRefs() to see if they're the same file/folder. You can then use FSRefMakePath() to get the canonical representation, but if you're displaying the filename to the user, you should use NSFileManager's -displayNameAtPath: method instead, since that handles localization and showing/hiding extensions properly.

Boaz Stuller
+7  A: 

There's really a couple of different parts to your question. By my reading, you want:

1 a way to tell if two different paths are the same on-disk file

2 a canonical name for the file on disk, with the proper casing

There's a third issue that gets mixed in, as well, having to do with Display Names, because in OS X a file can localize its name and appear differently for different locales. So let's add

3 a way to get the display name, because we might want to cache things depending on how the user sees the file system, not how the file system appears in the terminal.

We can solve 1 with the FSRef trick pointed out by @boaz-stuller. Or here's some code that does it using higher-level Cocoa calls, which saves us a little bit of memory juggling (since we can let the NSAutoreleasePool do it for us):

long getInode(NSString* path) {
 NSFileManager* fm = [NSFileManager defaultManager];
 NSError* error;
 NSDictionary* info = [fm attributesOfItemAtPath:path error:&error];
 NSNumber* inode = [info objectForKey:NSFileSystemFileNumber];
 return [inode longValue];
}

But to solve 2, we've got to use FSRefs to find out the canonical casing of the file:

NSString* getActualPath(NSString* path) {
 FSRef ref;
 OSStatus sts;
 UInt8* actualPath;

 //first get an FSRef for the path
 sts = FSPathMakeRef((const UInt8 *)[path UTF8String], &ref, NULL);
 if (sts) return [NSString stringWithFormat:@"Error #%d making ref.", sts];

 //then get a path from the FSRef
 actualPath = malloc(sizeof(UInt8)*MAX_PATH_LENGTH);
 sts = FSRefMakePath(&ref, actualPath, MAX_PATH_LENGTH);
 if (sts) return [NSString stringWithFormat:@"Error #%d making path.", sts];

 return [NSString stringWithUTF8String:(const char*)actualPath];
}

That's not bad at all, but we're still happy when we can solve 3 with Cocoa methods:

NSString* getDisplayPath(NSString* path) {
 NSFileManager* fm = [NSFileManager defaultManager];
 NSString* mine = [fm displayNameAtPath:path];
 NSString* parentPath = [path stringByDeletingLastPathComponent];
 NSString* parents = [@"/" isEqualToString:parentPath]
  ? @""
  : getDisplayPath(parentPath);
 return [NSString stringWithFormat:@"%@/%@", parents, mine];
}

Finally, we can add a bit of driver code and tie this all together into a CoreFoundation command line tool (I had to add the AppKit framework to get this to compile).

NSString* fileInfoString(NSString* path) {
 long inode = getInode(path);
 return [NSString stringWithFormat:
  @"\t%@  [inode #%d]\n\t\tis actually %@\n\t\tand displays as %@",
  path,
  inode,
  getActualPath(path),
  getDisplayPath(path)];
}

int main (int argc, const char * argv[]) {
 NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

 if (argc < 2) {
  NSLog(@"Usage: %s <path1> [<path2>]", argv[0]);
  return -1;
 }

 NSString* path1 = [NSString stringWithCString:argv[1]];
 NSString* path2 = argc > 2
  ? [NSString stringWithCString:argv[1]]
  : [path1 uppercaseString];

 long inode1 = getInode(path1);
 long inode2 = getInode(path2);

 NSString* prefix = [NSString stringWithFormat:
  @"Comparing Files:\n%@\n%@", 
  fileInfoString(path1), 
  fileInfoString(path2)];

 int retval = 0;
 if (inode1 == inode2) {
  NSLog(@"%@\nSame file.", prefix);
 } else {
  NSLog(@"%@\nDifferent files.", prefix);
  retval = 1;
 }

 [pool drain];
 return retval;
}

Now, we can put it all together and run it:

 $ checkpath /users/tal
 2008-12-15 23:59:10.605 checkpath[22375:10b] Comparing Files:
    /users/tal  [inode #1061692]
     is actually /Users/tal
     and displays as /Users/tal
    /USERS/TAL  [inode #1061692]
     is actually /Users/tal
     and displays as /Users/tal
 Same file.
TALlama
Wow, awesome answer! I didn't expect you to do so much for me. Thanks a million! getActualPath() is exactly what I needed.
FigBug
This code has a couple problems. 1) inodes are only unique per-device, so you really should compare device/inode pairs. 2) The malloc'ed actualPath is leaked. To fix, you can just declare it as UInt8 actualPath[PATH_MAX+1] instead.
Boaz Stuller
Also, I generally find FSRefs simpler to use than paths. Paths have a bunch of annoying edge cases (for an example, see this question :) ), while FSRefs mostly Just Work™. The only big limitation of FSRefs vs. paths is that paths can refer to files that don't exist yet, while FSRefs can't.
Boaz Stuller
No problem. Although, a simpler method (that didn't occur to me until just now) that would work as long as these keys are never shown to the user is to just lowercase all your paths before using them. Simple and effective!
TALlama
Except in case-sensitive filesystems. Paths are hard.
Boaz Stuller