Index: src/OFFileManager.m ================================================================== --- src/OFFileManager.m +++ src/OFFileManager.m @@ -338,11 +338,11 @@ } @catch (OFCreateDirectoryFailedException *e) { /* * If we didn't fail because any of the parents is missing, * there is no point in trying to create the parents. */ - if ([e errNo] != ENOENT) + if (e.errNo != ENOENT) @throw e; } components = [URL.URLEncodedPath componentsSeparatedByString: @"/"]; Index: src/OFURL.h ================================================================== --- src/OFURL.h +++ src/OFURL.h @@ -100,11 +100,11 @@ OFString *URLEncodedPath; /*! * @brief The path of the URL split into components. * - * The first component must always be empty to designate the root. + * The first component must always be `/` to designate the root. */ @property OF_NULLABLE_PROPERTY (readonly, copy, nonatomic) OFArray OF_GENERIC(OFString *) *pathComponents; /*! Index: src/OFURL.m ================================================================== --- src/OFURL.m +++ src/OFURL.m @@ -930,31 +930,48 @@ return _URLEncodedPath; } - (OFArray *)pathComponents { - return [self.path componentsSeparatedByString: @"/"]; + OFMutableArray *ret = [[[_URLEncodedPath + componentsSeparatedByString: @"/"] mutableCopy] autorelease]; + void *pool = objc_autoreleasePoolPush(); + size_t count = ret.count; + + if (count > 0 && [ret.firstObject length] == 0) + [ret replaceObjectAtIndex: 0 + withObject: @"/"]; + + for (size_t i = 0; i < count; i++) { + OFString *component = [ret objectAtIndex: i]; + [ret replaceObjectAtIndex: i + withObject: component.stringByURLDecoding]; + } + + [ret makeImmutable]; + + objc_autoreleasePoolPop(pool); + + return ret; } - (OFString *)lastPathComponent { void *pool = objc_autoreleasePoolPush(); - OFString *path = self.path; + OFString *path = _URLEncodedPath; const char *UTF8String, *lastComponent; size_t length; OFString *ret; if (path == nil) { objc_autoreleasePoolPop(pool); - return nil; } if ([path isEqual: @"/"]) { objc_autoreleasePoolPop(pool); - - return @""; + return @"/"; } if ([path hasSuffix: @"/"]) path = [path substringWithRange: of_range(0, path.length - 1)]; @@ -966,13 +983,14 @@ lastComponent = UTF8String + (length - i) + 1; break; } } - ret = [[OFString alloc] - initWithUTF8String: lastComponent - length: length - (lastComponent - UTF8String)]; + ret = [OFString + stringWithUTF8String: lastComponent + length: length - (lastComponent - UTF8String)]; + ret = [ret.stringByURLDecoding retain]; objc_autoreleasePoolPop(pool); return [ret autorelease]; } Index: tests/OFURLTests.m ================================================================== --- tests/OFURLTests.m +++ tests/OFURLTests.m @@ -25,18 +25,19 @@ @implementation TestsAppDelegate (OFURLTests) - (void)URLTests { OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init]; - OFURL *u1, *u2, *u3, *u4; + OFURL *u1, *u2, *u3, *u4, *u5; OFMutableURL *mu; TEST(@"+[URLWithString:]", R(u1 = [OFURL URLWithString: url_str]) && R(u2 = [OFURL URLWithString: @"http://foo:80"]) && R(u3 = [OFURL URLWithString: @"http://bar/"]) && - R(u4 = [OFURL URLWithString: @"file:///etc/passwd"])) + R(u4 = [OFURL URLWithString: @"file:///etc/passwd"]) && + R(u5 = [OFURL URLWithString: @"http://foo/bar/qux/foo%2fbar"])) EXPECT_EXCEPTION(@"+[URLWithString:] fails with invalid characters #1", OFInvalidFormatException, [OFURL URLWithString: @"ht,tp://foo"]) @@ -135,22 +136,25 @@ TEST(@"-[port]", [u1.port isEqual: [OFNumber numberWithUInt16: 1234]]) TEST(@"-[path]", [u1.path isEqual: @"/pa?th"] && [u4.path isEqual: @"/etc/passwd"]) TEST(@"-[pathComponents]", [u1.pathComponents isEqual: - [OFArray arrayWithObjects: @"", @"pa?th", nil]] && + [OFArray arrayWithObjects: @"/", @"pa?th", nil]] && [u4.pathComponents isEqual: - [OFArray arrayWithObjects: @"", @"etc", @"passwd", nil]]) + [OFArray arrayWithObjects: @"/", @"etc", @"passwd", nil]] && + [u5.pathComponents isEqual: + [OFArray arrayWithObjects: @"/", @"bar", @"qux", @"foo/bar", nil]]) TEST(@"-[lastPathComponent]", [[[OFURL URLWithString: @"http://host/foo//bar/baz"] lastPathComponent] isEqual: @"baz"] && [[[OFURL URLWithString: @"http://host/foo//bar/baz/"] lastPathComponent] isEqual: @"baz"] && [[[OFURL URLWithString: @"http://host/foo/"] lastPathComponent] isEqual: @"foo"] && [[[OFURL URLWithString: @"http://host/"] - lastPathComponent] isEqual: @""]) + lastPathComponent] isEqual: @"/"] && + [u5.lastPathComponent isEqual: @"foo/bar"]) TEST(@"-[query]", [u1.query isEqual: @"que#ry"] && u4.query == nil) TEST(@"-[fragment]", [u1.fragment isEqual: @"frag#ment"] && u4.fragment == nil)