Michael Tsai
Michael Tsai

Reputation: 2030

Does the isDirectory parameter to +[NSURL fileURLWithPath:isDirectory:] need to be correct?

I have long been using +[NSURL fileURLWithPath:] because it's convenient, but in profiling I found that this was a source of bottlenecks because it queries the filesystem before calling down to +[NSURL fileURLWithPath:isDirectory:] (or the Core Foundation equivalent):

This method assumes that path is a directory if it ends with a slash. If path does not end with a slash, the method examines the file system to determine if path is a file or a directory. If path exists in the file system and is a directory, the method appends a trailing slash. If path does not exist in the file system, the method assumes that it represents a file and does not append a trailing slash.

I would like to avoid this overhead if possible. But I don’t always know ahead of time whether the URL I’m constructing is a directory. For example, I may just be given a path, or I may be given a directory and the name of an item (of unknown type) therein. My question is, is it OK to always pass NO for isDirectory even though this may turn out not to be accurate?

In particular, I want to make sure this doesn’t mess up -[NSURL getResourceValue:forKey:error:] and NSFileManager.

Some basic testing did not reveal any bad consequences to this optimization, but that doesn’t mean there aren’t any. It’s not fully clear to me why NSURL cares about isDirectory in the first place. Looking at the CFURL source, the code seems to try to make sure that directory paths always end with a slash. But, other than for display purposes, it’s not clear to me why this would matter for filesystem paths. (This was never an issue when using NSString to represent paths.) The documentation for +[NSURL fileURLWithPath:isDirectory:] says that isDir is:

A Boolean value that specifies whether path is treated as a directory path when resolving against relative path components. Pass YES if the path indicates a directory, NO otherwise.

Indeed, if I use -[NSURL initWithString:relativeToURL:], the new URL does not include the last component of baseURL unless it was created with YES for isDirectory. However, I don’t think I’ve ever called this method, and it doesn’t seem like the system would need to do so on my behalf (for the above use cases). I note that -[NSURL URLByAppendingPathComponent:] inserts a slash if necessary, rather than assuming that the IS_DIRECTORY flag is correct. So, other than creating relative URLs, does this flag matter? Even if I wanted to, it does not seem possible in the general case to always pass in the correct value. The filesystem could always change out from under me. Nor can the system always determine it if I use +[NSURL fileURLWithPath:] because the path might not yet exist in the filesystem.

Upvotes: 9

Views: 1336

Answers (3)

Elise van Looij
Elise van Looij

Reputation: 4232

No, the isDirectory: parameter does not need to be correct, in my experience. Conversely, setting the isDirectory: parameter to YES, does not guarantee that url.path will end in a folder slash.

I've run a few tests on this and learned that it all depends on whether fileURLWithPath: can verify in the file system, and in a sandboxed application that may not always be possible.

That explains why this test in a sandboxed application has a perfect score, even though this is hardly the behaviour one would hope for:

- (void)testFileURLWithPathIsDirectory
{
    NSString *path = NSHomeDirectory();
    XCTAssertTrue([path isEqualToString:@"/Users/myUser"], @"%@", path);
    
    NSURL *pathURL = [NSURL fileURLWithPath:NSHomeDirectory() isDirectory:YES];
    XCTAssertTrue([pathURL.path isEqualToString:@"/Users/myUser"], @"%@", pathURL.path);
    
    XCTAssertFalse([pathURL.path isEqualToString:@"file:///Users/myUser/"], @"%@", pathURL.path);
}

So apparently, isDirectory should be considered a hint, and developers should keep in mind that fileURLWithPath: and fileURLWithPath:isDirectory: will produce different paths depending on the sandboxing status of the URL.

One last gotcha: the path of an URL pointing at a package will never have a trailing slash, but if the URL points at a file within a package, that path will show that a package is just a directory.

Upvotes: 0

Michael Tsai
Michael Tsai

Reputation: 2030

Here’s an example where it matters.

Upvotes: 0

Mike Abdullah
Mike Abdullah

Reputation: 15003

If you pass in NO and always create non-directory URLs, that's absolutely fine for any routines which use URLs to access the file system (e.g. -getResourceValue:forKey:error: or NSFileManager).

For file URLs, the trailing slash only really becomes important if you then want to resolve paths against the URL. i.e. this snippet:

NSURL *baseURL = [NSURL fileURLWithPath:path isDirectory:YES];
NSURL *result = [NSURL URLWithString:@"relative/path" relativeToURL:baseURL];

will produce different results to this one:

NSURL *baseURL = [NSURL fileURLWithPath:path isDirectory:NO];
NSURL *result = [NSURL URLWithString:@"relative/path" relativeToURL:baseURL];

In practice, dealing with relative strings/paths for file URLs tends not to be all that common anyway. Remote URLs need it a lot more (depends what your app does of course!)

Upvotes: 1

Related Questions