Index: src/OFMutableURL.m ================================================================== --- src/OFMutableURL.m +++ src/OFMutableURL.m @@ -22,10 +22,12 @@ #import "OFString.h" #import "OFURL+Private.h" #import "OFInvalidFormatException.h" +extern void of_url_verify_escaped(OFString *, OFCharacterSet *); + @implementation OFMutableURL @dynamic scheme, URLEncodedScheme, host, URLEncodedHost, port, user; @dynamic URLEncodedUser, password, URLEncodedPassword, path, URLEncodedPath; @dynamic pathComponents, query, URLEncodedQuery, fragment, URLEncodedFragment; @@ -40,38 +42,53 @@ } - (void)setScheme: (OFString *)scheme { void *pool = objc_autoreleasePoolPush(); + OFString *old = _URLEncodedScheme; + + _URLEncodedScheme = [[scheme stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLSchemeAllowedCharacterSet]] copy]; - [self setURLEncodedScheme: - [scheme stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLSchemeAllowedCharacterSet]]]; + [old release]; objc_autoreleasePoolPop(pool); } - (void)setURLEncodedScheme: (OFString *)URLEncodedScheme { - OFString *old = _URLEncodedScheme; + OFString *old; + + of_url_verify_escaped(URLEncodedScheme, + [OFCharacterSet URLSchemeAllowedCharacterSet]); + + old = _URLEncodedScheme; _URLEncodedScheme = [URLEncodedScheme copy]; [old release]; } - (void)setHost: (OFString *)host { void *pool = objc_autoreleasePoolPush(); + OFString *old = _URLEncodedHost; + + _URLEncodedHost = [[host stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLHostAllowedCharacterSet]] copy]; - [self setURLEncodedHost: [host stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLHostAllowedCharacterSet]]]; + [old release]; objc_autoreleasePoolPop(pool); } - (void)setURLEncodedHost: (OFString *)URLEncodedHost { - OFString *old = _URLEncodedHost; + OFString *old; + + of_url_verify_escaped(URLEncodedHost, + [OFCharacterSet URLHostAllowedCharacterSet]); + + old = _URLEncodedHost; _URLEncodedHost = [URLEncodedHost copy]; [old release]; } - (void)setPort: (OFNumber *)port @@ -82,55 +99,79 @@ } - (void)setUser: (OFString *)user { void *pool = objc_autoreleasePoolPush(); + OFString *old = _URLEncodedUser; + + _URLEncodedUser = [[user stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLUserAllowedCharacterSet]] copy]; - [self setURLEncodedUser: [user stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLUserAllowedCharacterSet]]]; + [old release]; objc_autoreleasePoolPop(pool); } - (void)setURLEncodedUser: (OFString *)URLEncodedUser { - OFString *old = _URLEncodedUser; + OFString *old; + + of_url_verify_escaped(URLEncodedUser, + [OFCharacterSet URLUserAllowedCharacterSet]); + + old = _URLEncodedUser; _URLEncodedUser = [URLEncodedUser copy]; [old release]; } - (void)setPassword: (OFString *)password { void *pool = objc_autoreleasePoolPush(); + OFString *old = _URLEncodedPassword; + + _URLEncodedPassword = [[password + stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLPasswordAllowedCharacterSet]] copy]; - [self setURLEncodedPassword: - [password stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLPasswordAllowedCharacterSet]]]; + [old release]; objc_autoreleasePoolPop(pool); } - (void)setURLEncodedPassword: (OFString *)URLEncodedPassword { - OFString *old = _URLEncodedPassword; + OFString *old; + + of_url_verify_escaped(URLEncodedPassword, + [OFCharacterSet URLPasswordAllowedCharacterSet]); + + old = _URLEncodedPassword; _URLEncodedPassword = [URLEncodedPassword copy]; [old release]; } - (void)setPath: (OFString *)path { void *pool = objc_autoreleasePoolPush(); + OFString *old = _URLEncodedPath; + + _URLEncodedPath = [[path stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLPathAllowedCharacterSet]] copy]; - [self setURLEncodedPath: [path stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLPathAllowedCharacterSet]]]; + [old release]; objc_autoreleasePoolPop(pool); } - (void)setURLEncodedPath: (OFString *)URLEncodedPath { - OFString *old = _URLEncodedPath; + OFString *old; + + of_url_verify_escaped(URLEncodedPath, + [OFCharacterSet URLPathAllowedCharacterSet]); + + old = _URLEncodedPath; _URLEncodedPath = [URLEncodedPath copy]; [old release]; } - (void)setPathComponents: (OFArray *)components @@ -154,39 +195,54 @@ } - (void)setQuery: (OFString *)query { void *pool = objc_autoreleasePoolPush(); + OFString *old = _URLEncodedQuery; + + _URLEncodedQuery = [[query stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLQueryAllowedCharacterSet]] copy]; - [self setURLEncodedQuery: - [query stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLQueryAllowedCharacterSet]]]; + [old release]; objc_autoreleasePoolPop(pool); } - (void)setURLEncodedQuery: (OFString *)URLEncodedQuery { - OFString *old = _URLEncodedQuery; + OFString *old; + + of_url_verify_escaped(URLEncodedQuery, + [OFCharacterSet URLQueryAllowedCharacterSet]); + + old = _URLEncodedQuery; _URLEncodedQuery = [URLEncodedQuery copy]; [old release]; } - (void)setFragment: (OFString *)fragment { void *pool = objc_autoreleasePoolPush(); + OFString *old = _URLEncodedFragment; + + _URLEncodedFragment = [[fragment + stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLFragmentAllowedCharacterSet]] copy]; - [self setURLEncodedFragment: - [fragment stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLFragmentAllowedCharacterSet]]]; + [old release]; objc_autoreleasePoolPop(pool); } - (void)setURLEncodedFragment: (OFString *)URLEncodedFragment { - OFString *old = _URLEncodedFragment; + OFString *old; + + of_url_verify_escaped(URLEncodedFragment, + [OFCharacterSet URLFragmentAllowedCharacterSet]); + + old = _URLEncodedFragment; _URLEncodedFragment = [URLEncodedFragment copy]; [old release]; } - (id)copy Index: src/OFURL.m ================================================================== --- src/OFURL.m +++ src/OFURL.m @@ -34,37 +34,54 @@ #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFOutOfMemoryException.h" static OFCharacterSet *URLAllowedCharacterSet = nil; +static OFCharacterSet *URLSchemeAllowedCharacterSet = nil; static OFCharacterSet *URLPathAllowedCharacterSet = nil; static OFCharacterSet *URLQueryOrFragmentAllowedCharacterSet = nil; -@interface OFCharacterSet_URLAllowed: OFCharacterSet +@interface OFCharacterSet_URLAllowedBase: OFCharacterSet +- (instancetype)of_init OF_METHOD_FAMILY(init); +@end + +@interface OFCharacterSet_URLAllowed: OFCharacterSet_URLAllowedBase + (OFCharacterSet *)URLAllowedCharacterSet; @end -@interface OFCharacterSet_URLPathAllowed: OFCharacterSet +@interface OFCharacterSet_URLSchemeAllowed: OFCharacterSet_URLAllowedBase ++ (OFCharacterSet *)URLSchemeAllowedCharacterSet; +@end + +@interface OFCharacterSet_URLPathAllowed: OFCharacterSet_URLAllowedBase + (OFCharacterSet *)URLPathAllowedCharacterSet; @end -@interface OFCharacterSet_URLQueryOrFragmentAllowed: OFCharacterSet +@interface OFCharacterSet_URLQueryOrFragmentAllowed: + OFCharacterSet_URLAllowedBase + (OFCharacterSet *)URLQueryOrFragmentAllowedCharacterSet; @end -@implementation OFCharacterSet_URLAllowed -+ (void)initialize +@interface OFCharacterSet_invertedSetWithPercent: OFCharacterSet { - if (self != [OFCharacterSet_URLAllowed class]) - return; - - URLAllowedCharacterSet = [[OFCharacterSet_URLAllowed alloc] init]; + OFCharacterSet *_characterSet; + bool (*_characterIsMember)(id, SEL, of_unichar_t); } -+ (OFCharacterSet *)URLAllowedCharacterSet +- (instancetype)of_initWithCharacterSet: (OFCharacterSet *)characterSet + OF_METHOD_FAMILY(init); +@end + +@implementation OFCharacterSet_URLAllowedBase +- (instancetype)init { - return URLAllowedCharacterSet; + OF_INVALID_INIT_METHOD +} + +- (instancetype)of_init +{ + return [super init]; } - (instancetype)autorelease { return self; @@ -81,10 +98,25 @@ - (unsigned int)retainCount { return OF_RETAIN_COUNT_MAX; } +@end + +@implementation OFCharacterSet_URLAllowed ++ (void)initialize +{ + if (self != [OFCharacterSet_URLAllowed class]) + return; + + URLAllowedCharacterSet = [[OFCharacterSet_URLAllowed alloc] of_init]; +} + ++ (OFCharacterSet *)URLAllowedCharacterSet +{ + return URLAllowedCharacterSet; +} - (bool)characterIsMember: (of_unichar_t)character { if (character < CHAR_MAX && of_ascii_isalnum(character)) return true; @@ -109,45 +141,57 @@ default: return false; } } @end + +@implementation OFCharacterSet_URLSchemeAllowed ++ (void)initialize +{ + if (self != [OFCharacterSet_URLSchemeAllowed class]) + return; + + URLSchemeAllowedCharacterSet = + [[OFCharacterSet_URLSchemeAllowed alloc] of_init]; +} + ++ (OFCharacterSet *)URLSchemeAllowedCharacterSet +{ + return URLSchemeAllowedCharacterSet; +} + +- (bool)characterIsMember: (of_unichar_t)character +{ + if (character < CHAR_MAX && of_ascii_isalnum(character)) + return true; + + switch (character) { + case '+': + case '-': + case '.': + return true; + default: + return false; + } +} +@end @implementation OFCharacterSet_URLPathAllowed + (void)initialize { if (self != [OFCharacterSet_URLPathAllowed class]) return; URLPathAllowedCharacterSet = - [[OFCharacterSet_URLPathAllowed alloc] init]; + [[OFCharacterSet_URLPathAllowed alloc] of_init]; } + (OFCharacterSet *)URLPathAllowedCharacterSet { return URLPathAllowedCharacterSet; } -- (instancetype)autorelease -{ - return self; -} - -- (instancetype)retain -{ - return self; -} - -- (void)release -{ -} - -- (unsigned int)retainCount -{ - return OF_RETAIN_COUNT_MAX; -} - - (bool)characterIsMember: (of_unichar_t)character { if (character < CHAR_MAX && of_ascii_isalnum(character)) return true; @@ -182,37 +226,18 @@ { if (self != [OFCharacterSet_URLQueryOrFragmentAllowed class]) return; URLQueryOrFragmentAllowedCharacterSet = - [[OFCharacterSet_URLQueryOrFragmentAllowed alloc] init]; + [[OFCharacterSet_URLQueryOrFragmentAllowed alloc] of_init]; } + (OFCharacterSet *)URLQueryOrFragmentAllowedCharacterSet { return URLQueryOrFragmentAllowedCharacterSet; } -- (instancetype)autorelease -{ - return self; -} - -- (instancetype)retain -{ - return self; -} - -- (void)release -{ -} - -- (unsigned int)retainCount -{ - return OF_RETAIN_COUNT_MAX; -} - - (bool)characterIsMember: (of_unichar_t)character { if (character < CHAR_MAX && of_ascii_isalnum(character)) return true; @@ -240,15 +265,60 @@ default: return false; } } @end + +@implementation OFCharacterSet_invertedSetWithPercent +- (instancetype)init +{ + OF_INVALID_INIT_METHOD +} + +- (instancetype)of_initWithCharacterSet: (OFCharacterSet *)characterSet +{ + self = [super init]; + + _characterSet = [characterSet retain]; + _characterIsMember = (bool (*)(id, SEL, of_unichar_t)) + [_characterSet methodForSelector: @selector(characterIsMember:)]; + + return self; +} + +- (void)dealloc +{ + [_characterSet release]; + + [super dealloc]; +} + +- (bool)characterIsMember: (of_unichar_t)character +{ + return (character != '%' && !_characterIsMember(_characterSet, + @selector(characterIsMember:), character)); +} +@end + +void +of_url_verify_escaped(OFString *string, OFCharacterSet *characterSet) +{ + void *pool = objc_autoreleasePoolPush(); + + characterSet = [[[OFCharacterSet_invertedSetWithPercent alloc] + of_initWithCharacterSet: characterSet] autorelease]; + + if ([string indexOfCharacterFromSet: characterSet] != OF_NOT_FOUND) + @throw [OFInvalidFormatException exception]; + + objc_autoreleasePoolPop(pool); +} @implementation OFCharacterSet (URLCharacterSets) + (OFCharacterSet *)URLSchemeAllowedCharacterSet { - return [OFCharacterSet_URLAllowed URLAllowedCharacterSet]; + return [OFCharacterSet_URLSchemeAllowed URLSchemeAllowedCharacterSet]; } + (OFCharacterSet *)URLHostAllowedCharacterSet { return [OFCharacterSet_URLAllowed URLAllowedCharacterSet]; @@ -352,10 +422,13 @@ _URLEncodedScheme = [[OFString alloc] initWithUTF8String: UTF8String length: tmp - UTF8String]; + of_url_verify_escaped(_URLEncodedScheme, + [OFCharacterSet URLSchemeAllowedCharacterSet]); + UTF8String = tmp + 3; if ((tmp = strchr(UTF8String, '/')) != NULL) { *tmp = '\0'; tmp++; @@ -373,14 +446,21 @@ _URLEncodedUser = [[OFString alloc] initWithUTF8String: UTF8String]; _URLEncodedPassword = [[OFString alloc] initWithUTF8String: tmp3]; + + of_url_verify_escaped(_URLEncodedPassword, + [OFCharacterSet + URLPasswordAllowedCharacterSet]); } else _URLEncodedUser = [[OFString alloc] initWithUTF8String: UTF8String]; + of_url_verify_escaped(_URLEncodedUser, + [OFCharacterSet URLUserAllowedCharacterSet]); + UTF8String = tmp2; } if ((tmp2 = strchr(UTF8String, ':')) != NULL) { OFString *portString; @@ -400,30 +480,44 @@ (uint16_t)[portString decimalValue]]; } else _URLEncodedHost = [[OFString alloc] initWithUTF8String: UTF8String]; + of_url_verify_escaped(_URLEncodedHost, + [OFCharacterSet URLHostAllowedCharacterSet]); + if ((UTF8String = tmp) != NULL) { if ((tmp = strchr(UTF8String, '#')) != NULL) { *tmp = '\0'; _URLEncodedFragment = [[OFString alloc] initWithUTF8String: tmp + 1]; + + of_url_verify_escaped(_URLEncodedFragment, + [OFCharacterSet + URLFragmentAllowedCharacterSet]); } if ((tmp = strchr(UTF8String, '?')) != NULL) { *tmp = '\0'; _URLEncodedQuery = [[OFString alloc] initWithUTF8String: tmp + 1]; + + of_url_verify_escaped(_URLEncodedQuery, + [OFCharacterSet + URLQueryAllowedCharacterSet]); } UTF8String--; *UTF8String = '/'; _URLEncodedPath = [[OFString alloc] initWithUTF8String: UTF8String]; + + of_url_verify_escaped(_URLEncodedPath, + [OFCharacterSet URLPathAllowedCharacterSet]); } objc_autoreleasePoolPop(pool); } @catch (id e) { [self release]; @@ -464,16 +558,22 @@ if ((tmp = strchr(UTF8String, '#')) != NULL) { *tmp = '\0'; _URLEncodedFragment = [[OFString alloc] initWithUTF8String: tmp + 1]; + + of_url_verify_escaped(_URLEncodedFragment, + [OFCharacterSet URLFragmentAllowedCharacterSet]); } if ((tmp = strchr(UTF8String, '?')) != NULL) { *tmp = '\0'; _URLEncodedQuery = [[OFString alloc] initWithUTF8String: tmp + 1]; + + of_url_verify_escaped(_URLEncodedQuery, + [OFCharacterSet URLQueryAllowedCharacterSet]); } if (*UTF8String == '/') _URLEncodedPath = [[OFString alloc] initWithUTF8String: UTF8String]; @@ -491,10 +591,13 @@ _URLEncodedPath = [[s stringByStandardizingURLPath] copy]; } + of_url_verify_escaped(_URLEncodedPath, + [OFCharacterSet URLPathAllowedCharacterSet]); + objc_autoreleasePoolPop(pool); } @catch (id e) { [self release]; @throw e; } @finally { Index: tests/OFURLTests.m ================================================================== --- tests/OFURLTests.m +++ tests/OFURLTests.m @@ -44,10 +44,30 @@ 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"])) + EXPECT_EXCEPTION(@"+[URLWithString:] fails with invalid characters #1", + OFInvalidFormatException, + [OFURL URLWithString: @"ht,tp://foo"]) + + EXPECT_EXCEPTION(@"+[URLWithString:] fails with invalid characters #2", + OFInvalidFormatException, + [OFURL URLWithString: @"http://f`oo"]) + + EXPECT_EXCEPTION(@"+[URLWithString:] fails with invalid characters #3", + OFInvalidFormatException, + [OFURL URLWithString: @"http://foo/`"]) + + EXPECT_EXCEPTION(@"+[URLWithString:] fails with invalid characters #4", + OFInvalidFormatException, + [OFURL URLWithString: @"http://foo/foo?`"]) + + EXPECT_EXCEPTION(@"+[URLWithString:] fails with invalid characters #5", + OFInvalidFormatException, + [OFURL URLWithString: @"http://foo/foo?foo#`"]) + TEST(@"+[URLWithString:relativeToURL:]", [[[OFURL URLWithString: @"/foo" relativeToURL: u1] string] isEqual: @"ht%3atp://us%3Aer:p%40w@ho%3Ast:1234/foo"] && [[[OFURL URLWithString: @"foo/bar?q" @@ -57,10 +77,34 @@ relativeToURL: [OFURL URLWithString: @"http://h/qux/?x"]] string] isEqual: @"http://h/qux/foo/bar"] && [[[OFURL URLWithString: @"http://foo/?q" relativeToURL: u1] string] isEqual: @"http://foo/?q"]) + EXPECT_EXCEPTION( + @"+[URLWithString:relativeToURL:] fails with invalid characters #1", + OFInvalidFormatException, + [OFURL URLWithString: @"`" + relativeToURL: u1]) + + EXPECT_EXCEPTION( + @"+[URLWithString:relativeToURL:] fails with invalid characters #2", + OFInvalidFormatException, + [OFURL URLWithString: @"/`" + relativeToURL: u1]) + + EXPECT_EXCEPTION( + @"+[URLWithString:relativeToURL:] fails with invalid characters #3", + OFInvalidFormatException, + [OFURL URLWithString: @"?`" + relativeToURL: u1]) + + EXPECT_EXCEPTION( + @"+[URLWithString:relativeToURL:] fails with invalid characters #4", + OFInvalidFormatException, + [OFURL URLWithString: @"#`" + relativeToURL: u1]) + #ifdef OF_HAVE_FILES TEST(@"+[fileURLWithPath:isDirectory:]", [[[OFURL fileURLWithPath: @"testfile.txt"] fileSystemRepresentation] isEqual: [[[OFFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent: @"testfile.txt"]]) @@ -116,32 +160,85 @@ TEST(@"-[setScheme:]", R([mu setScheme: @"ht:tp"]) && [[mu URLEncodedScheme] isEqual: @"ht%3Atp"]) + TEST(@"-[setURLEncodedScheme:]", + R([mu setURLEncodedScheme: @"ht%3Atp"]) && + [[mu scheme] isEqual: @"ht:tp"]) + + EXPECT_EXCEPTION( + @"-[setURLEncodedScheme:] with invalid characters fails", + OFInvalidFormatException, [mu setURLEncodedScheme: @"~"]) + TEST(@"-[setHost:]", R([mu setHost: @"ho:st"]) && [[mu URLEncodedHost] isEqual: @"ho%3Ast"]) + TEST(@"-[setURLEncodedHost:]", + R([mu setURLEncodedHost: @"ho%3Ast"]) && + [[mu host] isEqual: @"ho:st"]) + + EXPECT_EXCEPTION(@"-[setURLEncodedHost:] with invalid characters fails", + OFInvalidFormatException, [mu setURLEncodedHost: @"/"]) + TEST(@"-[setUser:]", R([mu setUser: @"us:er"]) && [[mu URLEncodedUser] isEqual: @"us%3Aer"]) + TEST(@"-[setURLEncodedUser:]", + R([mu setURLEncodedUser: @"us%3Aer"]) && + [[mu user] isEqual: @"us:er"]) + + EXPECT_EXCEPTION(@"-[setURLEncodedUser:] with invalid characters fails", + OFInvalidFormatException, [mu setURLEncodedHost: @"/"]) + TEST(@"-[setPassword:]", R([mu setPassword: @"pass:word"]) && [[mu URLEncodedPassword] isEqual: @"pass%3Aword"]) + TEST(@"-[setURLEncodedPassword:]", + R([mu setURLEncodedPassword: @"pass%3Aword"]) && + [[mu password] isEqual: @"pass:word"]) + + EXPECT_EXCEPTION( + @"-[setURLEncodedPassword:] with invalid characters fails", + OFInvalidFormatException, [mu setURLEncodedPassword: @"/"]) + TEST(@"-[setPath:]", R([mu setPath: @"pa/th@?"]) && [[mu URLEncodedPath] isEqual: @"pa/th@%3F"]) + TEST(@"-[setURLEncodedPath:]", + R([mu setURLEncodedPath: @"pa/th@%3F"]) && + [[mu path] isEqual: @"pa/th@?"]) + + EXPECT_EXCEPTION(@"-[setURLEncodedPath:] with invalid characters fails", + OFInvalidFormatException, [mu setURLEncodedPath: @"?"]) + TEST(@"-[setQuery:]", R([mu setQuery: @"que/ry?#"]) && [[mu URLEncodedQuery] isEqual: @"que/ry?%23"]) + TEST(@"-[setURLEncodedQuery:]", + R([mu setURLEncodedQuery: @"que/ry?%23"]) && + [[mu query] isEqual: @"que/ry?#"]) + + EXPECT_EXCEPTION( + @"-[setURLEncodedQuery:] with invalid characters fails", + OFInvalidFormatException, [mu setURLEncodedQuery: @"`"]) + TEST(@"-[setFragment:]", R([mu setFragment: @"frag/ment?#"]) && [[mu URLEncodedFragment] isEqual: @"frag/ment?%23"]) + TEST(@"-[setURLEncodedFragment:]", + R([mu setURLEncodedFragment: @"frag/ment?%23"]) && + [[mu fragment] isEqual: @"frag/ment?#"]) + + EXPECT_EXCEPTION( + @"-[setURLEncodedFragment:] with invalid characters fails", + OFInvalidFormatException, [mu setURLEncodedFragment: @"`"]) + [pool drain]; } @end