Index: src/OFEmbeddedURIHandler.m ================================================================== --- src/OFEmbeddedURIHandler.m +++ src/OFEmbeddedURIHandler.m @@ -74,11 +74,11 @@ @implementation OFEmbeddedURIHandler - (OFStream *)openItemAtURI: (OFURI *)URI mode: (OFString *)mode { const char *path; - if (![URI.scheme isEqual: @"objfw-embedded"] || URI.host != nil || + if (![URI.scheme isEqual: @"objfw-embedded"] || URI.host.length > 0 || URI.port != nil || URI.user != nil || URI.password != nil || URI.query != nil || URI.fragment != nil) @throw [OFInvalidArgumentException exception]; if (![mode isEqual: @"r"]) Index: src/OFURI.m ================================================================== --- src/OFURI.m +++ src/OFURI.m @@ -429,207 +429,217 @@ { return [[[self alloc] initFileURIWithPath: path isDirectory: isDirectory] autorelease]; } #endif + +static void +parseUserInfo(OFURI *self, const char *UTF8String, size_t length) +{ + const char *colon; + + if ((colon = memchr(UTF8String, ':', length)) != NULL) { + self->_percentEncodedUser = [[OFString alloc] + initWithUTF8String: UTF8String + length: colon - UTF8String]; + self->_percentEncodedPassword = [[OFString alloc] + initWithUTF8String: colon + 1 + length: length - (colon - UTF8String) - 1]; + + OFURIVerifyIsEscaped(self->_percentEncodedPassword, + [OFCharacterSet URIPasswordAllowedCharacterSet]); + } else + self->_percentEncodedUser = [[OFString alloc] + initWithUTF8String: UTF8String + length: length]; + + OFURIVerifyIsEscaped(self->_percentEncodedUser, + [OFCharacterSet URIUserAllowedCharacterSet]); +} + +static void +parseHostPort(OFURI *self, const char *UTF8String, size_t length) +{ + OFString *portString; + + if (*UTF8String == '[') { + const char *end = memchr(UTF8String, ']', length); + + if (end == NULL) + @throw [OFInvalidFormatException exception]; + + for (const char *iter = UTF8String + 1; iter < end; iter++) + if (!OFASCIIIsDigit(*iter) && *iter != ':' && + (*iter < 'a' || *iter > 'f') && + (*iter < 'A' || *iter > 'F')) + @throw [OFInvalidFormatException exception]; + + self->_percentEncodedHost = [[OFString alloc] + initWithUTF8String: UTF8String + length: end - UTF8String + 1]; + + length -= (end - UTF8String) + 1; + UTF8String = end + 1; + } else { + const char *colon = memchr(UTF8String, ':', length); + + if (colon != NULL) { + self->_percentEncodedHost = [[OFString alloc] + initWithUTF8String: UTF8String + length: colon - UTF8String]; + + length -= colon - UTF8String; + UTF8String = colon; + } else { + self->_percentEncodedHost = [[OFString alloc] + initWithUTF8String: UTF8String + length: length]; + + UTF8String += length; + length = 0; + } + + OFURIVerifyIsEscaped(self->_percentEncodedHost, + [OFCharacterSet URIHostAllowedCharacterSet]); + } + + if (length == 0) + return; + + if (length <= 1 || *UTF8String != ':') + @throw [OFInvalidFormatException exception]; + + UTF8String++; + length--; + + for (size_t i = 0; i < length; i++) + if (!OFASCIIIsDigit(UTF8String[i])) + @throw [OFInvalidFormatException exception]; + + portString = [OFString stringWithUTF8String: UTF8String length: length]; + + if (portString.unsignedLongLongValue > 65535) + @throw [OFInvalidFormatException exception]; + + self->_port = [[OFNumber alloc] initWithUnsignedShort: + (unsigned short)portString.unsignedLongLongValue]; +} + +static size_t +parseAuthority(OFURI *self, const char *UTF8String, size_t length) +{ + size_t ret; + const char *slash, *at; + + if ((slash = memchr(UTF8String, '/', length)) != NULL) + length = slash - UTF8String; + + ret = length; + + if ((at = memchr(UTF8String, '@', length)) != NULL) { + parseUserInfo(self, UTF8String, at - UTF8String); + + length -= at - UTF8String + 1; + UTF8String = at + 1; + } + + parseHostPort(self, UTF8String, length); + + return ret; +} + +static void +parsePathQueryFragment(OFURI *self, const char *UTF8String, size_t length) +{ + const char *fragment, *query; + + if ((fragment = memchr(UTF8String, '#', length)) != NULL) { + self->_percentEncodedFragment = [[OFString alloc] + initWithUTF8String: fragment + 1 + length: length - (fragment - UTF8String) - 1]; + + OFURIVerifyIsEscaped(self->_percentEncodedFragment, + [OFCharacterSet URIQueryAllowedCharacterSet]); + + length = fragment - UTF8String; + } + + if ((query = memchr(UTF8String, '?', length)) != NULL) { + self->_percentEncodedQuery = [[OFString alloc] + initWithUTF8String: query + 1 + length: length - (query - UTF8String) - 1]; + + OFURIVerifyIsEscaped(self->_percentEncodedQuery, + [OFCharacterSet URIFragmentAllowedCharacterSet]); + + length = query - UTF8String; + } + + self->_percentEncodedPath = [[OFString alloc] + initWithUTF8String: UTF8String + length: length]; + + OFURIVerifyIsEscaped(self->_percentEncodedPath, + [OFCharacterSet URIQueryAllowedCharacterSet]); +} - (instancetype)initWithString: (OFString *)string { - char *UTF8String, *UTF8String2 = NULL; - self = [super init]; @try { void *pool = objc_autoreleasePoolPush(); - char *tmp, *tmp2; - bool isIPv6Host = false; - - UTF8String = UTF8String2 = OFStrDup(string.UTF8String); - - if ((tmp = strchr(UTF8String, ':')) == NULL) - @throw [OFInvalidFormatException exception]; - - if (strncmp(tmp, "://", 3) != 0) - @throw [OFInvalidFormatException exception]; - - for (tmp2 = UTF8String; tmp2 < tmp; tmp2++) - *tmp2 = OFASCIIToLower(*tmp2); - - _percentEncodedScheme = [[OFString alloc] - initWithUTF8String: UTF8String - length: tmp - UTF8String]; + const char *UTF8String = string.UTF8String; + size_t length = string.UTF8StringLength; + const char *colon; + + if ((colon = strchr(UTF8String, ':')) == NULL) + @throw [OFInvalidFormatException exception]; + + _percentEncodedScheme = [[[OFString + stringWithUTF8String: UTF8String + length: colon - UTF8String] lowercaseString] + copy]; OFURIVerifyIsEscaped(_percentEncodedScheme, [OFCharacterSet URISchemeAllowedCharacterSet]); - UTF8String = tmp + 3; - - if ((tmp = strchr(UTF8String, '/')) != NULL) { - *tmp = '\0'; - tmp++; - } - - if ((tmp2 = strchr(UTF8String, '@')) != NULL) { - char *tmp3; - - *tmp2 = '\0'; - tmp2++; - - if ((tmp3 = strchr(UTF8String, ':')) != NULL) { - *tmp3 = '\0'; - tmp3++; - - _percentEncodedUser = [[OFString alloc] - initWithUTF8String: UTF8String]; - _percentEncodedPassword = [[OFString alloc] - initWithUTF8String: tmp3]; - - OFURIVerifyIsEscaped(_percentEncodedPassword, - [OFCharacterSet - URIPasswordAllowedCharacterSet]); - } else - _percentEncodedUser = [[OFString alloc] - initWithUTF8String: UTF8String]; - - OFURIVerifyIsEscaped(_percentEncodedUser, - [OFCharacterSet URIUserAllowedCharacterSet]); - - UTF8String = tmp2; - } - - if (UTF8String[0] == '[') { - tmp2 = UTF8String++; - - while (OFASCIIIsDigit(*UTF8String) || - *UTF8String == ':' || - (*UTF8String >= 'a' && *UTF8String <= 'f') || - (*UTF8String >= 'A' && *UTF8String <= 'F')) - UTF8String++; - - if (*UTF8String != ']') - @throw [OFInvalidFormatException exception]; - - UTF8String++; - - _percentEncodedHost = [[OFString alloc] - initWithUTF8String: tmp2 - length: UTF8String - tmp2]; - - if (*UTF8String == ':') { - OFString *portString; - - tmp2 = ++UTF8String; - - while (*UTF8String != '\0') { - if (!OFASCIIIsDigit(*UTF8String)) - @throw [OFInvalidFormatException - exception]; - - UTF8String++; - } - - portString = [OFString - stringWithUTF8String: tmp2 - length: UTF8String - tmp2]; - - if (portString.length == 0 || - portString.unsignedLongLongValue > 65535) - @throw [OFInvalidFormatException - exception]; - - _port = [[OFNumber alloc] initWithUnsignedShort: - portString.unsignedLongLongValue]; - } else if (*UTF8String != '\0') - @throw [OFInvalidFormatException exception]; - - isIPv6Host = true; - } else if ((tmp2 = strchr(UTF8String, ':')) != NULL) { - OFString *portString; - - *tmp2 = '\0'; - tmp2++; - - _percentEncodedHost = [[OFString alloc] - initWithUTF8String: UTF8String]; - - portString = [OFString stringWithUTF8String: tmp2]; - - if (portString.unsignedLongLongValue > 65535) - @throw [OFInvalidFormatException exception]; - - _port = [[OFNumber alloc] initWithUnsignedShort: - portString.unsignedLongLongValue]; - } else { - _percentEncodedHost = [[OFString alloc] - initWithUTF8String: UTF8String]; - - if (_percentEncodedHost.length == 0) { - [_percentEncodedHost release]; - _percentEncodedHost = nil; - } - } - - if (_percentEncodedHost != nil && !isIPv6Host) - OFURIVerifyIsEscaped(_percentEncodedHost, - [OFCharacterSet URIHostAllowedCharacterSet]); - - if ((UTF8String = tmp) != NULL) { - if ((tmp = strchr(UTF8String, '#')) != NULL) { - *tmp = '\0'; - - _percentEncodedFragment = [[OFString alloc] - initWithUTF8String: tmp + 1]; - - OFURIVerifyIsEscaped(_percentEncodedFragment, - [OFCharacterSet - URIFragmentAllowedCharacterSet]); - } - - if ((tmp = strchr(UTF8String, '?')) != NULL) { - *tmp = '\0'; - - _percentEncodedQuery = [[OFString alloc] - initWithUTF8String: tmp + 1]; - - OFURIVerifyIsEscaped(_percentEncodedQuery, - [OFCharacterSet - URIQueryAllowedCharacterSet]); - } - - /* - * Some versions of GCC issue a false-positive warning - * (turned error) about a string overflow. This is a - * false positive because UTF8String is set to tmp - * above and tmp is either NULL or points *after* the - * slash for the path. So all we do here is go back to - * that slash and restore it. - */ -#if OF_GCC_VERSION >= 402 -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wpragmas" -# pragma GCC diagnostic ignored "-Wunknown-warning-option" -# pragma GCC diagnostic ignored "-Wstringop-overflow" -#endif - UTF8String--; - *UTF8String = '/'; -#if OF_GCC_VERSION >= 402 -# pragma GCC diagnostic pop -#endif - + length -= colon - UTF8String + 1; + UTF8String = colon + 1; + + if (length >= 2 && UTF8String[0] == '/' && + UTF8String[1] == '/') { + size_t authorityLength; + + UTF8String += 2; + length -= 2; + + authorityLength = parseAuthority(self, + UTF8String, length); + + UTF8String += authorityLength; + length -= authorityLength; + + if (length > 0) + OFEnsure(UTF8String[0] == '/'); + } + + if (length > 0 && UTF8String[0] == '/') + parsePathQueryFragment(self, UTF8String, length); + else { _percentEncodedPath = [[OFString alloc] - initWithUTF8String: UTF8String]; + initWithUTF8String: UTF8String + length: length]; OFURIVerifyIsEscaped(_percentEncodedPath, [OFCharacterSet URIPathAllowedCharacterSet]); } objc_autoreleasePoolPop(pool); } @catch (id e) { [self release]; @throw e; - } @finally { - OFFreeMemory(UTF8String2); } return self; } @@ -684,11 +694,11 @@ stringByAppendingString: relativePath] copy]; else { OFMutableString *path = [OFMutableString stringWithString: - (URI->_percentEncodedPath != nil + (URI->_percentEncodedPath.length > 0 ? URI->_percentEncodedPath : @"/")]; OFRange range = [path rangeOfString: @"/" options: OFStringSearchBackwards]; @@ -1118,11 +1128,15 @@ - (OFString *)string { OFMutableString *ret = [OFMutableString string]; - [ret appendFormat: @"%@://", _percentEncodedScheme]; + [ret appendFormat: @"%@:", _percentEncodedScheme]; + + if (_percentEncodedHost != nil || _port != nil || + _percentEncodedUser != nil || _percentEncodedPassword != nil) + [ret appendString: @"//"]; if (_percentEncodedUser != nil && _percentEncodedPassword != nil) [ret appendFormat: @"%@:%@@", _percentEncodedUser, _percentEncodedPassword]; @@ -1132,16 +1146,12 @@ if (_percentEncodedHost != nil) [ret appendString: _percentEncodedHost]; if (_port != nil) [ret appendFormat: @":%@", _port]; - if (_percentEncodedPath != nil) { - if (![_percentEncodedPath hasPrefix: @"/"]) - @throw [OFInvalidFormatException exception]; - + if (_percentEncodedPath != nil) [ret appendString: _percentEncodedPath]; - } if (_percentEncodedQuery != nil) [ret appendFormat: @"?%@", _percentEncodedQuery]; if (_percentEncodedFragment != nil) Index: tests/OFURITests.m ================================================================== --- tests/OFURITests.m +++ tests/OFURITests.m @@ -23,21 +23,25 @@ @implementation TestsAppDelegate (OFURITests) - (void)URITests { void *pool = objc_autoreleasePoolPush(); - OFURI *URI1, *URI2, *URI3, *URI4, *URI5, *URI6, *URI7; + OFURI *URI1, *URI2, *URI3, *URI4, *URI5, *URI6, *URI7, *URI8, *URI9; + OFURI *URI10; OFMutableURI *mutableURI; TEST(@"+[URIWithString:]", R(URI1 = [OFURI URIWithString: URIString]) && R(URI2 = [OFURI URIWithString: @"http://foo:80"]) && R(URI3 = [OFURI URIWithString: @"http://bar/"]) && R(URI4 = [OFURI URIWithString: @"file:///etc/passwd"]) && R(URI5 = [OFURI URIWithString: @"http://foo/bar/qux/foo%2fbar"]) && R(URI6 = [OFURI URIWithString: @"https://[12:34::56:abcd]/"]) && - R(URI7 = [OFURI URIWithString: @"https://[12:34::56:abcd]:234/"])) + R(URI7 = [OFURI URIWithString: @"https://[12:34::56:abcd]:234/"]) && + R(URI8 = [OFURI URIWithString: @"urn:qux:foo"]) && + R(URI9 = [OFURI URIWithString: @"file:/foo?query#frag"]) && + R(URI10 = [OFURI URIWithString: @"file:foo@bar/qux"])) EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #1", OFInvalidFormatException, [OFURI URIWithString: @"ht,tp://foo"]) @@ -66,10 +70,18 @@ [OFURI URIWithString: @"https://[f]:/"]) EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #8", OFInvalidFormatException, [OFURI URIWithString: @"https://[f]:f/"]) + + EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #9", + OFInvalidFormatException, + [OFURI URIWithString: @"foo:bar?qux"]) + + EXPECT_EXCEPTION(@"+[URIWithString:] fails with invalid characters #10", + OFInvalidFormatException, + [OFURI URIWithString: @"foo:bar#qux"]) TEST(@"+[URIWithString:relativeToURI:]", [[[OFURI URIWithString: @"/foo" relativeToURI: URI1] string] isEqual: @"ht%3atp://us%3Aer:p%40w@ho%3Ast:1234/foo"] && [[[OFURI URIWithString: @"foo/bar?q" @@ -136,26 +148,40 @@ TEST(@"-[string]", [URI1.string isEqual: URIString] && [URI2.string isEqual: @"http://foo:80"] && [URI3.string isEqual: @"http://bar/"] && - [URI4.string isEqual: @"file:///etc/passwd"]) + [URI4.string isEqual: @"file:///etc/passwd"] && + [URI5.string isEqual: @"http://foo/bar/qux/foo%2fbar"] && + [URI6.string isEqual: @"https://[12:34::56:abcd]/"] && + [URI7.string isEqual: @"https://[12:34::56:abcd]:234/"] && + [URI8.string isEqual: @"urn:qux:foo"] && + [URI9.string isEqual: @"file:/foo?query#frag"] && + [URI10.string isEqual: @"file:foo@bar/qux"]) TEST(@"-[scheme]", - [URI1.scheme isEqual: @"ht:tp"] && [URI4.scheme isEqual: @"file"]) + [URI1.scheme isEqual: @"ht:tp"] && [URI4.scheme isEqual: @"file"] && + [URI9.scheme isEqual: @"file"] && [URI10.scheme isEqual: @"file"]) - TEST(@"-[user]", [URI1.user isEqual: @"us:er"] && URI4.user == nil) + TEST(@"-[user]", [URI1.user isEqual: @"us:er"] && URI4.user == nil && + URI10.user == nil) TEST(@"-[password]", - [URI1.password isEqual: @"p@w"] && URI4.password == nil) + [URI1.password isEqual: @"p@w"] && URI4.password == nil && + URI10.password == nil) TEST(@"-[host]", [URI1.host isEqual: @"ho:st"] && [URI6.host isEqual: @"12:34::56:abcd"] && - [URI7.host isEqual: @"12:34::56:abcd"]) + [URI7.host isEqual: @"12:34::56:abcd"] && + URI8.host == nil && URI9.host == nil && URI10.host == nil) TEST(@"-[port]", URI1.port.unsignedShortValue == 1234 && - [URI4 port] == nil && URI7.port.unsignedShortValue == 234) + [URI4 port] == nil && URI7.port.unsignedShortValue == 234 && + URI8.port == nil && URI9.port == nil && URI10.port == nil) TEST(@"-[path]", [URI1.path isEqual: @"/pa?th"] && - [URI4.path isEqual: @"/etc/passwd"]) + [URI4.path isEqual: @"/etc/passwd"] && + [URI8.path isEqual: @"qux:foo"] && + [URI9.path isEqual: @"/foo"] && + [URI10.path isEqual: @"foo@bar/qux"]) TEST(@"-[pathComponents]", [URI1.pathComponents isEqual: [OFArray arrayWithObjects: @"/", @"pa?th", nil]] && [URI4.pathComponents isEqual: [OFArray arrayWithObjects: @"/", @"etc", @"passwd", nil]] && @@ -170,17 +196,19 @@ lastPathComponent] isEqual: @"foo"] && [[[OFURI URIWithString: @"http://host/"] lastPathComponent] isEqual: @"/"] && [URI5.lastPathComponent isEqual: @"foo/bar"]) TEST(@"-[query]", - [URI1.query isEqual: @"que#ry=1&f&oo=b=ar"] && URI4.query == nil) + [URI1.query isEqual: @"que#ry=1&f&oo=b=ar"] && URI4.query == nil && + [URI9.query isEqual: @"query"]) TEST(@"-[queryItems]", [URI1.queryItems isEqual: [OFArray arrayWithObjects: [OFPair pairWithFirstObject: @"que#ry" secondObject: @"1"], [OFPair pairWithFirstObject: @"f&oo" secondObject: @"b=ar"], nil]]); TEST(@"-[fragment]", - [URI1.fragment isEqual: @"frag#ment"] && URI4.fragment == nil) + [URI1.fragment isEqual: @"frag#ment"] && URI4.fragment == nil && + [URI9.fragment isEqual: @"frag"] && URI10.fragment == nil) TEST(@"-[copy]", R(URI4 = [[URI1 copy] autorelease])) TEST(@"-[isEqual:]", [URI1 isEqual: URI4] && ![URI2 isEqual: URI3] && [[OFURI URIWithString: @"HTTP://bar/"] isEqual: URI3])