Index: src/OFMutableURL.m ================================================================== --- src/OFMutableURL.m +++ src/OFMutableURL.m @@ -68,12 +68,17 @@ - (void)setHost: (OFString *)host { void *pool = objc_autoreleasePoolPush(); OFString *old = _URLEncodedHost; - _URLEncodedHost = [[host stringByURLEncodingWithAllowedCharacters: - [OFCharacterSet URLHostAllowedCharacterSet]] copy]; + if (of_url_is_ipv6_host(host)) + _URLEncodedHost = [[OFString alloc] + initWithFormat: @"[%@]", host]; + else + _URLEncodedHost = [[host + stringByURLEncodingWithAllowedCharacters: + [OFCharacterSet URLHostAllowedCharacterSet]] copy]; [old release]; objc_autoreleasePoolPop(pool); } @@ -80,11 +85,16 @@ - (void)setURLEncodedHost: (OFString *)URLEncodedHost { OFString *old; - if (URLEncodedHost != nil) + if ([URLEncodedHost hasPrefix: @"["] && + [URLEncodedHost hasSuffix: @"]"]) { + if (!of_url_is_ipv6_host([URLEncodedHost substringWithRange: + of_range(1, URLEncodedHost.length - 2)])) + @throw [OFInvalidFormatException exception]; + } else if (URLEncodedHost != nil) of_url_verify_escaped(URLEncodedHost, [OFCharacterSet URLHostAllowedCharacterSet]); old = _URLEncodedHost; _URLEncodedHost = [URLEncodedHost copy]; Index: src/OFURL.h ================================================================== --- src/OFURL.h +++ src/OFURL.h @@ -340,9 +340,17 @@ * * @return The characters allowed in the fragment part of a URL. */ + (OFCharacterSet *)URLFragmentAllowedCharacterSet; @end + +#ifdef __cplusplus +extern "C" { +#endif +extern bool of_url_is_ipv6_host(OFString *host); +#ifdef __cplusplus +} +#endif OF_ASSUME_NONNULL_END #import "OFMutableURL.h" Index: src/OFURL.m ================================================================== --- src/OFURL.m +++ src/OFURL.m @@ -67,10 +67,31 @@ } - (instancetype)of_initWithCharacterSet: (OFCharacterSet *)characterSet OF_METHOD_FAMILY(init); @end + +bool +of_url_is_ipv6_host(OFString *host) +{ + const char *UTF8String = host.UTF8String; + bool hasColon = false; + + while (*UTF8String != '\0') { + if (!of_ascii_isdigit(*UTF8String) && *UTF8String != ':' && + (*UTF8String < 'a' || *UTF8String > 'f') && + (*UTF8String < 'A' || *UTF8String > 'F')) + return false; + + if (*UTF8String == ':') + hasColon = true; + + UTF8String++; + } + + return hasColon; +} @implementation OFURLAllowedCharacterSetBase - (instancetype)init { OF_INVALID_INIT_METHOD @@ -829,10 +850,21 @@ return _URLEncodedScheme; } - (OFString *)host { + if ([_URLEncodedHost hasPrefix: @"["] && + [_URLEncodedHost hasSuffix: @"]"]) { + OFString *host = [_URLEncodedHost substringWithRange: + of_range(1, _URLEncodedHost.length - 2)]; + + if (!of_url_is_ipv6_host(host)) + @throw [OFInvalidArgumentException exception]; + + return host; + } + return _URLEncodedHost.stringByURLDecoding; } - (OFString *)URLEncodedHost { Index: tests/OFURLTests.m ================================================================== --- tests/OFURLTests.m +++ tests/OFURLTests.m @@ -152,12 +152,12 @@ TEST(@"-[user]", [u1.user isEqual: @"us:er"] && u4.user == nil) TEST(@"-[password]", [u1.password isEqual: @"p@w"] && u4.password == nil) TEST(@"-[host]", [u1.host isEqual: @"ho:st"] && - [u6.host isEqual: @"[12:34::56:abcd]"] && - [u7.host isEqual: @"[12:34::56:abcd]"]) + [u6.host isEqual: @"12:34::56:abcd"] && + [u7.host isEqual: @"12:34::56:abcd"]) TEST(@"-[port]", [u1.port isEqual: [OFNumber numberWithUInt16: 1234]] && [u4 port] == nil && [u7.port isEqual: [OFNumber numberWithUInt16: 234]]) TEST(@"-[path]", [u1.path isEqual: @"/pa?th"] && [u4.path isEqual: @"/etc/passwd"]) @@ -204,17 +204,31 @@ EXPECT_EXCEPTION( @"-[setURLEncodedScheme:] with invalid characters fails", OFInvalidFormatException, mu.URLEncodedScheme = @"~") TEST(@"-[setHost:]", - (mu.host = @"ho:st") && [mu.URLEncodedHost isEqual: @"ho%3Ast"]) + (mu.host = @"ho:st") && [mu.URLEncodedHost isEqual: @"ho%3Ast"] && + (mu.host = @"12:34:ab") && + [mu.URLEncodedHost isEqual: @"[12:34:ab]"] && + (mu.host = @"12:34:aB") && + [mu.URLEncodedHost isEqual: @"[12:34:aB]"] && + (mu.host = @"12:34:g") && + [mu.URLEncodedHost isEqual: @"12%3A34%3Ag"]) TEST(@"-[setURLEncodedHost:]", - (mu.URLEncodedHost = @"ho%3Ast") && [mu.host isEqual: @"ho:st"]) + (mu.URLEncodedHost = @"ho%3Ast") && [mu.host isEqual: @"ho:st"] && + (mu.URLEncodedHost = @"[12:34]") && [mu.host isEqual: @"12:34"] && + (mu.URLEncodedHost = @"[12::ab]") && [mu.host isEqual: @"12::ab"]) + + EXPECT_EXCEPTION(@"-[setURLEncodedHost:] with invalid characters fails" + " #1", OFInvalidFormatException, mu.URLEncodedHost = @"/") + + EXPECT_EXCEPTION(@"-[setURLEncodedHost:] with invalid characters fails" + " #2", OFInvalidFormatException, mu.URLEncodedHost = @"[12:34") - EXPECT_EXCEPTION(@"-[setURLEncodedHost:] with invalid characters fails", - OFInvalidFormatException, mu.URLEncodedHost = @"/") + EXPECT_EXCEPTION(@"-[setURLEncodedHost:] with invalid characters fails" + " #3", OFInvalidFormatException, mu.URLEncodedHost = @"[a::g]") TEST(@"-[setUser:]", (mu.user = @"us:er") && [mu.URLEncodedUser isEqual: @"us%3Aer"]) TEST(@"-[setURLEncodedUser:]",