How to Fix fileReferenceURL() to Work with NSURL in Swift 3 and Swift 4

I am upgrading the code base of the Word Counter to Swift 3. Yeah, you read that right. I didn't touch the Swift code base for almost 2 years now. Horrible, I know – and I'm punished for deferring this so long in every module I try to convert and build.

One very interesting problem was runtime crashes in a submodule I build where URLs were nil all of a sudden. This code from 2015 (!!) used to work:

public struct LocalURL {
    public let URL: NSURL

    public init(URL: NSURL) {
        assert(URL.fileURL)

        if URL.isFileReferenceURL() {
            self.URL = URL
        } else {
            self.URL = URL.fileReferenceURL()!
        }
    }
    // ...
}

Yeah, some unicorns are dying from my code thanks to force unwrapping. This is not the only place I did that. I was a terrible Swift citizen in 2015, it turns out.

Apart from my low Swift !-standards, this did use to work. Now it doesn't. Even for an existing file path, URL.fileReferenceURL() just returns the same URL as you put in; this is clearly not what I was aiming for, since fileReferenceURL() is supposed to convert existing file URLs to a path-independent pointer to a file, aka a "file reference". These look something like file:///.file/id=6571367.437879/ instead of file:///tmp/test.txt.

After casting from URL to NSURL and back for a while, I discovered Swift Bug SR-2728: apparently this is a known problem since Swift 3. Seems to be related to the bridging between NSURL, which is an NSObject subclass, and URL, which is a Swift struct.

An annoyingly verbose workaround by Charles Srstka for Swift 3.1 is to perform all the work in the Objective-C runtime:

if let refURL = (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL {
    print(refURL) // will print something along the lines of 'file:///.file/id=01234546.789012345'
}

That does indeed work! So if you have to work on a Swift 3.1 codebase and encounter this, there you go.

Swift 4.1 has a simpler mechanism to ensure you get a NSURL instead of a URL – the only type that supports file reference URLs as of September 2018, still.

if let fileRefURL = (url as NSURL).fileReferenceURL() as NSURL? { 
    print(fileRefURL)
}

Try that in the Swift 4.1 REPL to see that it works. Whew.

I do understand that there may be reasons to remove fileReferenceURL from URL and leave it on NSURL, but when you do invoke it on NSURL, I think it should at least return another NSURL object that works as expected instead of bridging to Swift's URL struct that, for some reason, won't work.

Interestingly enough, if you do know the file reference, e.g. file:///.file/id=6571367.437879/, you can work with Swift's plain URL just fine:

let url = URL(string: "file:///.file/id=6571367.437879/")
print(url?.path)
// Output: 
//   /private/etc/tmp/test.txt

So when Swift's Foundation URL type supports getting a path from a file reference URL, why does the fileReferenceURL() stuff not work?

Beats me!

If you happen to know something more, I'd be happy to know about the secret in the comments.

Browse the blog archive