To start, when I refer to inode numbers I’m specifically referring to the
st_ino
field returned by
stat
and anything that’s defined to match that. Because this stuff has evolved over time there’s a bunch of different terms floating around and I want to be specific about what I’m referring too.
Originally macOS only supported 32-bit inode numbers. That changed to 64-bit a long time ago (Mac OS X 10.5), at least for new code compiled with the latest tools (there’s a compatibility layer that helped with the transition).
HFS Plus supports a 32-bit catalogue node ID (CNID). This is roughly analogous to the UNIX inode number concept and has been used as the inode number since the dawn of macOS.
Note CNIDs and inodes do differ in a few edge cases, like hard links and resource forks.
The File Manager API, the one introduced in Mac OS 9, exposes the 32-bit CNID. This is what you’re seeing in the
nodeID
field of
FSCatalogInfo
. macOS deals with this in one of two ways:
For file systems that support CNIDs natively (HFS, HFS Plus, AFS), it uses the CNID (although I’m a little fuzzy on what it does in the above-mentioned edge cases).
For other file systems, like MS-DOS/FAT, it dynamically creates an in-memory map between these CNID values and the path. This map is known as the file ID tree.
APFS has 64-bit inode numbers. These do not change across a restart. However, if you access APFS via the File Manager, there’s a problem because the File Manager can only return a 32-bit value and APFS uses all 64-bits of the inode number. In this case the File Manager uses the file ID tree to generate CNIDs.
This explains why we really want folks moving away from the File Manager; the file ID tree is a compatibility path that’s both slow and non-persistent.
Now, coming back to your main issue, you wrote:
I store the number (and its data) in a binary search tree written to disk.
This is a supported use case, albeit one that’s relatively obscure (although there is one notable example of it, the Spotlight index!). To get this working you’ll need to do four things:
Refuse to work on volumes where
NSURLVolumeSupportsPersistentIDsKey
is false (or come up with some alternative strategy for such volumes)Expand your index to support 64-bits
Get the inode number, not the CNID
Use
fsgetpath
to map from an inode number to a path so you can then use other file system APIs to work with the file (this is logically equivalent to the File Manager FSResolveNodeID
routine).
I recommend that you also use this opportunity to remove all uses of File Manager from your app. As much as I love that API, it’s time to move on.
In terms of what APIs you should be using, I try to avoid older
NSFileManager
APIs, like
NSFileSystemFileNumber
, preferring to use
NSURL
instead. That means using
NSURL
for most low-level stuff and
NSFileManager
for high-level operations. Unfortunately the modern APIs don’t have good support for your use case, so it’s likely you will need some assistance from lower-level APIs like
fsgetpath
.
Finally, be aware that
fsgetpath
was introduced relatively recently (10.13). If you need to support older systems then you can use a compatibility shim that relies on the volfs support in older versions of the OS (see
QA1113).
WARNING Always use
fsgetpath
if it’s available. volfs-style paths work just fine on legacy systems but we can’t guarantee that they’ll work forever.
Pasted in below is a code snippet that shows this technique.
Share and Enjoy
—
Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
let myEmail = "eskimo" + "1" + "@apple.com"
static ssize_t fsgetpath_legacy(char * buf, size_t buflen, fsid_t * fsid, uint64_t obj_id) {
char volfsPath[64]; // 8 for `/.vol//\0`, 10 for `fsid->val[0]`, 20 for `obj_id`, rounded up for paranoia
snprintf(volfsPath, sizeof(volfsPath), "/.vol/%ld/%llu", (long) fsid->val[0], (unsigned long long) obj_id);
struct {
uint32_t length;
attrreference_t pathRef;
char buffer[MAXPATHLEN];
} __attribute__((aligned(4), packed)) attrBuf;
struct attrlist attrList;
memset(&attrList, 0, sizeof(attrList));
attrList.bitmapcount = ATTR_BIT_MAP_COUNT;
attrList.commonattr = ATTR_CMN_FULLPATH;
bool success = getattrlist(volfsPath, &attrList, &attrBuf, sizeof(attrBuf), 0) == 0;
if ( ! success ) {
return -1;
}
if (attrBuf.pathRef.attr_length > buflen) {
errno = ENOSPC;
return -1;
}
strlcpy(buf, ((const char *) &attrBuf.pathRef) + attrBuf.pathRef.attr_dataoffset, buflen);
return attrBuf.pathRef.attr_length;
}
static ssize_t fsgetpath_compat(char * buf, size_t buflen, fsid_t * fsid, uint64_t obj_id) {
if (__builtin_available(macOS 10.13, *)) {
return fsgetpath(buf, buflen, fsid, obj_id);
} else {
return fsgetpath_legacy(buf, buflen, fsid, obj_id);
}
}