NSURL is relatively bad

July 19 2022 by Jeff Johnson

I've written about NSURL once before. I did not have good things to say. I do not have good things to say this time either.

Here's the documentation for the resourceSpecifier property of NSURL:

This property contains the resource specifier. Any percent-encoded characters are not unescaped. For example, in the URL http://www.example.com/index.html?key1=value1#jumplink, the resource specifier is //www.example.com/index.html?key1=value1#jumplink (everything after the colon).

Important
If the receiver does not specify a net location portion of the URL, as returned by the toll-free bridged CFURL function CFURLCopyNetLocation, then this method returns only the path of the receiver. For example, in the URL file:///file.txt, the resource specifier is /file.txt.

This is reiterated to an extent in the NSURL.h header file:

/* Any URL is composed of these two basic pieces.  
The full URL would be the concatenation of [myURL scheme], ':', [myURL resourceSpecifier] */ @property (nullable, readonly, copy) NSString *scheme; @property (nullable, readonly, copy) NSString *resourceSpecifier;

The problem is that if you use [NSURL URLWithString:relativeToURL:] rather than [NSURL URLWithString:] to get an NSURL instance, you can get unexpected results for the resourceSpecifier property:

#import <Foundation/Foundation.h>
int main(int argc, const char *argv[]) {
  @autoreleasepool {
    NSURL *base = [NSURL URLWithString:@"https://www.apple.com"];
    NSURL *relative = [NSURL URLWithString:@"/mac/" relativeToURL:base];
    NSURL *absolute = [relative absoluteURL];
    NSLog(@"relative: %@", [relative resourceSpecifier]);
    NSLog(@"absolute: %@", [absolute resourceSpecifier]);
  }
  return 0;
}

Here's the output of the above program:

relative: /mac/
absolute: //www.apple.com/mac/

The catch is that the relative receiver does specify a net location portion of the URL… in the base URL. So why doesn't NSURL resolve against the base URL to find the resourceSpecifier? Why…

Anyway, the workaround, as you can see above, is to get the absoluteURL first before checking the resourceSpecifier.

Jeff Johnson (My apps, PayPal.Me)