Index: src/OFURL.h ================================================================== --- src/OFURL.h +++ src/OFURL.h @@ -238,10 +238,36 @@ * @return An Initialized OFURL */ - (instancetype)initFileURLWithPath: (OFString *)path isDirectory: (bool)isDirectory; #endif + +/*! + * @brief Returns a new URL with the specified path component appended. + * + * If the URL is a file URL, the file system is queried whether the appended + * component is a directory. + * + * @param component The path component to append. If it starts with the slash, + * the component is not appended, but replaces the path + * instead. + * @return A new URL with the specified path component appended + */ +- (OFURL *)URLByAppendingPathComponent: (OFString *)component; + +/*! + * @brief Returns a new URL with the specified path component appended. + * + * @param component The path component to append. If it starts with the slash, + * the component is not appended, but replaces the path + * instead. + * @param isDirectory Whether the appended component is a directory, meaning + * that the URL path should have a trailing slash + * @return A new URL with the specified path component appended + */ +- (OFURL *)URLByAppendingPathComponent: (OFString *)component + isDirectory: (bool)isDirectory; @end @interface OFCharacterSet (URLCharacterSets) #ifdef OF_HAVE_CLASS_PROPERTIES @property (class, readonly, nonatomic) Index: src/OFURL.m ================================================================== --- src/OFURL.m +++ src/OFURL.m @@ -993,10 +993,78 @@ objc_autoreleasePoolPop(pool); return [path autorelease]; } + +- (OFMutableURL *)of_URLByAppendingPathComponent: (OFString *)component +{ + OFMutableURL *ret = [[self mutableCopy] autorelease]; + void *pool; + OFMutableString *URLEncodedPath; + + if ([component hasPrefix: @"/"]) { + [ret setPath: component]; + return ret; + } + + pool = objc_autoreleasePoolPush(); + URLEncodedPath = [[[self URLEncodedPath] mutableCopy] autorelease]; + + if (![URLEncodedPath hasSuffix: @"/"]) + [URLEncodedPath appendString: @"/"]; + + [URLEncodedPath appendString: + [component stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLPathAllowedCharacterSet]]]; + + [ret setURLEncodedPath: URLEncodedPath]; + + objc_autoreleasePoolPop(pool); + + return ret; +} + +- (OFURL *)URLByAppendingPathComponent: (OFString *)component +{ + OFMutableURL *ret = [self of_URLByAppendingPathComponent: component]; + +#ifdef OF_HAVE_FILES + if ([[ret scheme] isEqual: @"file"]) { + void *pool = objc_autoreleasePoolPush(); + + if ([[OFFileManager defaultManager] directoryExistsAtURL: ret]) + [ret setURLEncodedPath: [[ret URLEncodedPath] + stringByAppendingString: @"/"]]; + + objc_autoreleasePoolPop(pool); + } +#endif + + [ret makeImmutable]; + + return ret; +} + +- (OFURL *)URLByAppendingPathComponent: (OFString *)component + isDirectory: (bool)isDirectory +{ + OFMutableURL *ret = [self of_URLByAppendingPathComponent: component]; + + if (isDirectory) { + void *pool = objc_autoreleasePoolPush(); + + [ret setURLEncodedPath: + [[ret URLEncodedPath] stringByAppendingString: @"/"]]; + + objc_autoreleasePoolPop(pool); + } + + [ret makeImmutable]; + + return ret; +} - (OFString *)description { return [OFString stringWithFormat: @"<%@: %@>", [self class], [self string]]; Index: tests/OFURLTests.m ================================================================== --- tests/OFURLTests.m +++ tests/OFURLTests.m @@ -236,9 +236,39 @@ [[mu fragment] isEqual: @"frag/ment?#"]) EXPECT_EXCEPTION( @"-[setURLEncodedFragment:] with invalid characters fails", OFInvalidFormatException, [mu setURLEncodedFragment: @"`"]) + + TEST(@"-[URLByAppendingPathComponent:isDirectory:]", + [[[OFURL URLWithString: @"file:///foo/bar"] + URLByAppendingPathComponent: @"qux" + isDirectory: false] isEqual: + [OFURL URLWithString: @"file:///foo/bar/qux"]] && + [[[OFURL URLWithString: @"file:///foo/bar/"] + URLByAppendingPathComponent: @"qux" + isDirectory: false] isEqual: + [OFURL URLWithString: @"file:///foo/bar/qux"]] && + [[[OFURL URLWithString: @"file:///foo/bar/"] + URLByAppendingPathComponent: @"qu?x" + isDirectory: false] isEqual: + [OFURL URLWithString: @"file:///foo/bar/qu%3Fx"]] && + [[[OFURL URLWithString: @"file:///foo/bar/"] + URLByAppendingPathComponent: @"qu?x" + isDirectory: true] isEqual: + [OFURL URLWithString: @"file:///foo/bar/qu%3Fx/"]] && + [[[OFURL URLWithString: @"file:///foo/bar/"] + URLByAppendingPathComponent: @"/qux" + isDirectory: false] isEqual: + [OFURL URLWithString: @"file:///qux"]] && + [[[OFURL URLWithString: @"file:///foo/bar/"] + URLByAppendingPathComponent: @"/qu?x" + isDirectory: false] isEqual: + [OFURL URLWithString: @"file:///qu%3Fx"]] && + [[[OFURL URLWithString: @"file:///foo/bar/"] + URLByAppendingPathComponent: @"/qu?x" + isDirectory: true] isEqual: + [OFURL URLWithString: @"file:///qu%3Fx/"]]) [pool drain]; } @end