Ky -
Ky -

Reputation: 32163

How do I see if some folder is a child of another using NSURL?

I have an [URL] which represent a set of special parent directories. I am given another [URL] which represents files scattered around the system. I want to know if any of these files are in any of my special directories, or any of their subdirectories. Is there a simple/intended way to do this, without manually parsing/traversing an absolute URL's path?

Upvotes: 4

Views: 1848

Answers (3)

s3cur3
s3cur3

Reputation: 3035

A slightly improved, Swift 5 version combining nice features of both Umair and rmaddy's existing excellent answers.

Like Umair's answer, this correctly handles the case where a folder in the path is a partial match (that is, it will not say that /foo/bar-baz/bang has /foo/bar/ as an ancestor).

It also performs the canonicalization (standardizing paths to eliminate things like . or ../, and resolving symlinks) like rmaddy's answer.

func pathHasAncestor(maybeChild: URL, maybeAncestor: URL) -> Bool {
    let ancestorComponents: [String] = canonicalize(maybeAncestor).pathComponents
    let childComponents: [String] = canonicalize(maybeChild).pathComponents

    return ancestorComponents.count < childComponents.count
        && !zip(ancestorComponents, childComponents).contains(where: !=)
}

func canonicalize(_ url: URL) -> URL {
    url.standardizedFileURL.resolvingSymlinksInPath()
}

Here's a very simple unit test to verify the basic functionality:

import XCTest

class FileUtilsTests: XCTestCase {
    func testPathHasAncestor() throws {
        let leaf = URL(fileURLWithPath: "/foo/bar/baz")
        let parent = leaf.deletingLastPathComponent()
        XCTAssertTrue(pathHasAncestor(maybeChild: leaf, maybeAncestor: parent))
        XCTAssertFalse(pathHasAncestor(maybeChild: URL(fileURLWithPath: "/foo/bar 1/baz"), maybeAncestor: parent))
        XCTAssertFalse(pathHasAncestor(maybeChild: leaf, maybeAncestor: leaf))
    }
}

Upvotes: 6

Umair
Umair

Reputation: 1253

The problem with the prefix solution is that it would fail in cases: eg.

PathA: /Documents/untitled folder (1)/SomeFolder/Xyz/Waterfall.mp3
PathB: /Documents/untitled folder

The prefix solution will tell you that the PathA is child of PathB, although that's not the case.

A manually traversing solution:

Swift:

extension String {
    func isChildPath(of path: String) -> Bool {
        self.count < path.count
            && !zip(URL(fileURLWithPath: self).pathComponents,
                    URL(fileURLWithPath: path).pathComponents)
                .contains(where: !=)
    }
}

Objective-C:

@interface NSString (Additions)
-(BOOL)isChildOfPath:(NSString *)path;
@end
@implementation NSString (Additions)
-(BOOL)isChildOfPath:(NSString *)path {
   NSString * child = self;
   NSString * parent = path;
   NSArray<NSString *> * childPathComponents = child.pathComponents;
   NSArray<NSString *> * parentPathComponents = parent.pathComponents;
   if (parentPathComponents.count >= childPathComponents.count) {
        return NO;
   }

   for (NSInteger i = 0; i < parentPathComponents.count; i++) {
       NSString * parentComponent = parentPathComponents[i];
       NSString * childComponent = childPathComponents[i];
       if ( ![parentComponent isEqualToString:childComponent] ) {
           return NO;
       }
   }
   return YES;
}
@end

Upvotes: 2

rmaddy
rmaddy

Reputation: 318944

There's no method in NSURL that lets you see if another NSURL represents the root path of the other.

One possible solution is to convert the two URLs to path strings using the path property. Then see if one string is the prefix of the other. But before getting the URL's path, use both URLByStandardizingPath and URLByResolvingSymlinksInPath to ensure a consistent result.

Example:

NSURL *specialURL = ... // a URL for one of the special parent directories
NSURL *fileURL = ... // a URL for one of the files to check
NSString *specialPath = [specialURL.URLByStandardizingPath.URLByResolvingSymlinksInPath.path stringByAppendingString:@"/"];
NSString *filePath = fileURL.URLByStandardizingPath.URLByResolvingSymlinksInPath.path
if ([filePath hasPrefix:specialPath]) {
    // This file is in this special directory
}

Upvotes: 6

Related Questions