Replacement for FSGetCatalogInfo() and kFSCatInfoNodeID

For decades I have been using the code below but FSGetCatalogInfo() is now marked as deprecated.

Is there a modern replacement to obtain the file node ID?

Is the file node ID implemented when using APFS?


Simplified code:

FSCatalogInfo catalogInfo;


// Get the node ID

OSErr result = FSGetCatalogInfo(ref, kFSCatInfoNodeFlags | kFSCatInfoNodeID, &catalogInfo, NULL, NULL, NULL);


UInt32 fileID = catalogInfo.nodeID;

Replies

To start, this code is likely to have problems on APFS because APFS supports 64-bit inode numbers, and there’s no way to represent that in the legacy File Manager API.

What are you doing with these node IDs? In a lot of cases the best replacement here is a bookmark — it takes care of all the fiddly details associated with tracking an item on various volumes formats. However, that’s not the right in all cases, so I can’t offer a concrete suggestion without more details.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

My program is an audio player and I use the inode number to refer to files.

I store the number (and its data) in a binary search tree written to disk.

I've used the same code since back in the Mac OS 8/9 days and also on Windows.

It works very good.

I can start using 64-bit inode numbers and I found out that "NSFileManager / NSFileSystemFileNumber" is the modern way to get the inode number and it's identical to the FSGetCatalogInfo() as long as the number fits in 32-bits.


And if "kCFURLVolumeSupportsPersistentIDsKey" returns true, the inode number should never change between reboots of the system, is this a correct assumption?



This is how it works with my development computer (and always did) but I don't have access to a system that uses APFS.

I have recently rewritten my app into 64-bit and for one of my customers that uses the APFS, the inode number changes between reboots of the system even though "kCFURLVolumeSupportsPersistentIDsKey" returns true.



Why does his system change the inode number between reboots?

That's the big question I'm trying to solve, any idea?


If the inode number isn't persistent between reboots, then I have to change the way I associate the file and its data.

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);
    }
}

Eskimo, thanks a lot!

The explanation was super clear and I have everything working perfect.

Why not publish that info as a technote?

Quinn, what's wrong with just using this API, which is supposed to work in all earlier OSX versions just as well:


#include <sys/syscall.h>
#define fsgetpath(buf, bufsize, fsid, objid)  \
  (ssize_t)syscall(SYS_fsgetpath, buf, (size_t)bufsize, fsid, (uint64_t)objid)


(A former file systems engineer from Apple, JL, recommended using this, versus using the volfs approach).


There's also a similar definition in Darwin's xnu/bsd/sys/fsgetpath.h

I’m aware of this

syscall
-based approach but I’m not happy recommending it to folks. Before I start, I want to be crystal clear that Apple’s platforms do not support binary compatibility at the
syscall
layer. Our binary compatibility stake in the ground is the System framework. That is not necessarily a problem here — you’re only doing this on a compatibility path — but it’s important to keep in mind in general.

With regards this specific issue, the

syscall
-based approach has a few drawbacks:
  • Using the macro as you’ve shown here causes you to use

    syscall
    rather than
    fsgetpath
    even on systems that actually support
    fsgetpath
    . That’s not cool because, as I mentioned above,
    syscall
    is not a compatibility guaranteed. You must rename the macro (to something like
    fsgetpath_legacy
    ) if you want to use this approach.
  • Arguable it’s not public API, so I’ve no idea what App Review will make of it.

  • Any

    syscall
    use is frowned upon, a fact reflected in its availability macros.
    syscall
    is not available on half our platforms (watchOS and tvOS) and is officially deprecated on the other half.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"