Url: implement same URL parsing logic in C/C++ code

- Remove uses of group matching regular expression, not available on all
  build hosts,
- Parsing is faster than our old regexp engine.

Signed-off-by: Adrien Destugues <pulkomandy@pulkomandy.tk>

- Fixes #13002
- Fixed some indentation (tabs vs space), please configure your editor
  properly.
This commit is contained in:
Andrew Lindesay 2016-10-13 22:59:16 +13:00 committed by Adrien Destugues
parent f004acb098
commit cf65729463
4 changed files with 554 additions and 107 deletions

View File

@ -104,7 +104,7 @@ public:
private:
void _ResetFields();
bool _ContainsDelimiter(const BString& url);
void _ExplodeUrlString(const BString& urlString);
status_t _ExplodeUrlString(const BString& urlString);
BString _MergePath(const BString& relative) const;
void _SetPathUnsafe(const BString& path);

View File

@ -21,7 +21,6 @@
#ifdef HAIKU_TARGET_PLATFORM_HAIKU
#include <ICUWrapper.h>
#endif
#include <RegExp.h>
#ifdef HAIKU_TARGET_PLATFORM_HAIKU
#include <unicode/idna.h>
@ -506,11 +505,17 @@ BUrl::IsValid() const
if (!fHasProtocol)
return false;
if (fProtocol == "http" || fProtocol == "https" || fProtocol == "ftp")
return fHasHost;
if (fProtocol == "http" || fProtocol == "https" || fProtocol == "ftp"
|| fProtocol == "ipp" || fProtocol == "afp" || fProtocol == "telnet"
|| fProtocol == "gopher" || fProtocol == "nntp" || fProtocol == "sftp"
|| fProtocol == "finger" || fProtocol == "pop" || fProtocol == "imap") {
return fHasHost && !fHost.IsEmpty();
}
// TODO: Implement for real!
return fHasHost || fHasPath;
if (fProtocol == "file")
return fHasPath;
return true;
}
@ -909,75 +914,194 @@ BUrl::_ContainsDelimiter(const BString& url)
}
void
enum explode_url_parse_state {
EXPLODE_PROTOCOL,
EXPLODE_PROTOCOLTERMINATOR,
EXPLODE_AUTHORITYORPATH,
EXPLODE_AUTHORITY,
EXPLODE_PATH,
EXPLODE_REQUEST, // query
EXPLODE_FRAGMENT,
EXPLODE_ERROR,
EXPLODE_COMPLETE
};
typedef bool (*explode_char_match_fn)(char c);
static bool
explode_is_protocol_char(char c)
{
return isalnum(c) || c == '+' || c == '.' || c == '-';
}
static bool
explode_is_authority_char(char c)
{
return !(c == '/' || c == '?' || c == '#');
}
static bool
explode_is_path_char(char c)
{
return !(c == '#' || c == '?');
}
static bool
explode_is_request_char(char c)
{
return c != '#';
}
static int32
char_offset_until_fn_false(const char* url, int32 len, int32 offset,
explode_char_match_fn fn)
{
while (offset < len && fn(url[offset]))
offset++;
return offset;
}
/*
* This function takes a URL in string-form and parses the components of the URL out.
*/
status_t
BUrl::_ExplodeUrlString(const BString& url)
{
// The regexp is provided in RFC3986 (URI generic syntax), Appendix B
static RegExp urlMatcher(
"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?");
_ResetFields();
// RFC3986, Appendix C; the URL should not contain whitespace or delimiters
// by this point.
if (_ContainsDelimiter(url))
return; // TODO error handing
return B_BAD_VALUE;
RegExp::MatchResult match = urlMatcher.Match(url.String());
explode_url_parse_state state = EXPLODE_PROTOCOL;
int32 offset = 0;
int32 length = url.Length();
const char *url_c = url.String();
if (!match.HasMatched())
return; // TODO error reporting
// The regexp is provided in RFC3986 (URI generic syntax), Appendix B
// ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?
// The ensuing logic attempts to simulate the behaviour of extracting the groups
// from the string without requiring a group-capable regex engine.
// Scheme/Protocol
url.CopyInto(fProtocol, match.GroupStartOffsetAt(1),
match.GroupEndOffsetAt(1) - match.GroupStartOffsetAt(1));
while (state != EXPLODE_ERROR && offset < length) {
switch (state) {
if (!_IsProtocolValid()) {
fHasProtocol = false;
fProtocol.Truncate(0);
} else
fHasProtocol = true;
// Authority (including user credentials, host, and port
if (match.GroupEndOffsetAt(2) - match.GroupStartOffsetAt(2) > 0)
case EXPLODE_PROTOCOL:
{
url.CopyInto(fAuthority, match.GroupStartOffsetAt(3),
match.GroupEndOffsetAt(3) - match.GroupStartOffsetAt(3));
SetAuthority(fAuthority);
int32 end_protocol = char_offset_until_fn_false(url_c, length,
offset, explode_is_protocol_char);
if (end_protocol < length) {
SetProtocol(BString(&url_c[offset], end_protocol - offset));
state = EXPLODE_PROTOCOLTERMINATOR;
offset = end_protocol;
} else {
fHasHost = false;
fHasPort = false;
fHasUserName = false;
fHasPassword = false;
#if DEBUG
fprintf(stderr,
"unexpected end of url when parsing the protocol\n");
#endif
state = EXPLODE_ERROR;
}
break;
}
// Path
url.CopyInto(fPath, match.GroupStartOffsetAt(4),
match.GroupEndOffsetAt(4) - match.GroupStartOffsetAt(4));
if (!fPath.IsEmpty())
fHasPath = true;
// Query
if (match.GroupEndOffsetAt(5) - match.GroupStartOffsetAt(5) > 0)
case EXPLODE_PROTOCOLTERMINATOR:
{
url.CopyInto(fRequest, match.GroupStartOffsetAt(6),
match.GroupEndOffsetAt(6) - match.GroupStartOffsetAt(6));
fHasRequest = true;
if (url[offset] == ':') {
state = EXPLODE_AUTHORITYORPATH;
offset++;
} else {
fRequest = "";
fHasRequest = false;
#ifdef DEBUG
fprintf(stderr,
"unexpected character '%c' terminating the protocol\n",
url_c[offset]);
#endif
state = EXPLODE_ERROR;
}
break;
}
// Fragment
if (match.GroupEndOffsetAt(7) - match.GroupStartOffsetAt(7) > 0)
case EXPLODE_AUTHORITYORPATH:
{
url.CopyInto(fFragment, match.GroupStartOffsetAt(8),
match.GroupEndOffsetAt(8) - match.GroupStartOffsetAt(8));
fHasFragment = true;
if (strncmp(&url_c[offset], "//", 2) == 0) {
state = EXPLODE_AUTHORITY;
offset += 2;
} else {
fFragment = "";
fHasFragment = false;
state = EXPLODE_PATH;
}
break;
}
case EXPLODE_AUTHORITY:
{
int end_authority = char_offset_until_fn_false(url_c, length,
offset, explode_is_authority_char);
SetAuthority(BString(&url_c[offset], end_authority - offset));
state = EXPLODE_PATH;
offset = end_authority;
break;
}
case EXPLODE_PATH:
{
int end_path = char_offset_until_fn_false(url_c, length, offset,
explode_is_path_char);
SetPath(BString(&url_c[offset], end_path - offset));
state = EXPLODE_REQUEST;
offset = end_path;
break;
}
case EXPLODE_REQUEST: // query
{
if (url_c[offset] == '?') {
offset++;
int end_request = char_offset_until_fn_false(url_c, length,
offset, explode_is_request_char);
SetRequest(BString(&url_c[offset], end_request - offset));
offset = end_request;
}
state = EXPLODE_FRAGMENT;
break;
}
case EXPLODE_FRAGMENT:
{
if (url_c[offset] == '#') {
offset++;
SetFragment(BString(&url_c[offset], length - offset));
offset = length;
}
state = EXPLODE_COMPLETE;
break;
}
case EXPLODE_ERROR:
case EXPLODE_COMPLETE:
// should never be reached - keeps the compiler happy
break;
}
}
if(state == EXPLODE_ERROR) {
#ifdef DEBUG
fprintf(stderr, "failure to explode url\n");
#endif
_ResetFields();
return B_BAD_VALUE;
}
return B_OK;
}
@ -1011,62 +1135,173 @@ BUrl::_SetPathUnsafe(const BString& path)
}
enum authority_parse_state {
AUTHORITY_USERNAME,
AUTHORITY_PASSWORD,
AUTHORITY_HOST,
AUTHORITY_PORT,
AUTHORITY_COMPLETE
};
static bool
authority_is_username_char(char c)
{
return !(c == ':' || c == '@');
}
static bool
authority_is_password_char(char c)
{
return !(c == '@');
}
static bool
authority_is_ipv6_host_char(char c) {
return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')
|| (c >= '0' && c <= '9') || c == ':';
}
static bool
authority_is_host_char(char c) {
return !(c == ':' || c == '/');
}
static bool
authority_is_port_char(char c) {
return c >= '0' && c <= '9';
}
void
BUrl::SetAuthority(const BString& authority)
{
fAuthority = authority;
fUser.Truncate(0);
fPassword.Truncate(0);
fHost.Truncate(0);
fPort = 0;
fHasPort = false;
fHasUserName = false;
fHasPassword = false;
bool hasUsernamePassword = B_ERROR != fAuthority.FindFirst('@');
authority_parse_state state = AUTHORITY_USERNAME;
int32 offset = 0;
int32 length = authority.Length();
const char *authority_c = authority.String();
while (AUTHORITY_COMPLETE != state && offset < length) {
switch (state) {
case AUTHORITY_USERNAME:
{
if (hasUsernamePassword) {
int32 end_username = char_offset_until_fn_false(
authority_c, length, offset,
authority_is_username_char);
SetUserName(BString(&authority_c[offset],
end_username - offset));
state = AUTHORITY_PASSWORD;
offset = end_username;
} else {
state = AUTHORITY_HOST;
}
break;
}
case AUTHORITY_PASSWORD:
{
if (hasUsernamePassword && ':' == authority[offset]) {
offset++; // move past the delimiter
int32 end_password = char_offset_until_fn_false(
authority_c, length, offset,
authority_is_password_char);
SetPassword(BString(&authority_c[offset],
end_password - offset));
offset = end_password;
}
// if the host was preceded by a username + password couple
// then there will be an '@' delimiter to avoid.
if (authority_c[offset] == '@') {
offset++;
}
state = AUTHORITY_HOST;
break;
}
case AUTHORITY_HOST:
{
// the host may be enclosed within brackets in order to express
// an IPV6 address.
if (authority_c[offset] == '[') {
int32 end_ipv6_host = char_offset_until_fn_false(
authority_c, length, offset + 1,
authority_is_ipv6_host_char);
if (authority_c[end_ipv6_host] == ']') {
SetHost(BString(&authority_c[offset],
(end_ipv6_host - offset) + 1));
state = AUTHORITY_PORT;
offset = end_ipv6_host + 1;
}
}
// if an IPV6 host was not found.
if (AUTHORITY_HOST == state) {
int32 end_host = char_offset_until_fn_false(
authority_c, length, offset, authority_is_host_char);
SetHost(BString(&authority_c[offset], end_host - offset));
state = AUTHORITY_PORT;
offset = end_host;
}
break;
}
case AUTHORITY_PORT:
{
if (authority_c[offset] == ':') {
offset++;
int32 end_port = char_offset_until_fn_false(
authority_c, length, offset, authority_is_port_char);
SetPort(atoi(&authority_c[offset]));
offset = end_port;
}
state = AUTHORITY_COMPLETE;
break;
}
case AUTHORITY_COMPLETE:
// should never be reached - keeps the compiler happy
break;
}
}
// An empty authority is still an authority, making it possible to have
// URLs such as file:///path/to/file.
// TODO however, there is no way to unset the authority once it is set...
// We may want to take a const char* parameter and allow NULL.
fHasHost = true;
if (fAuthority.IsEmpty())
return;
int32 userInfoEnd = fAuthority.FindFirst('@');
int16 hostAndPortStart = 0;
// URL contains userinfo field
if (userInfoEnd != -1) {
BString userInfo;
fAuthority.CopyInto(userInfo, 0, userInfoEnd);
int16 colonDelimiter = userInfo.FindFirst(':', 0);
if (colonDelimiter == 0) {
SetPassword(userInfo);
} else if (colonDelimiter != -1) {
userInfo.CopyInto(fUser, 0, colonDelimiter);
userInfo.CopyInto(fPassword, colonDelimiter + 1,
userInfo.Length() - colonDelimiter);
SetUserName(fUser);
SetPassword(fPassword);
} else {
SetUserName(fUser);
}
hostAndPortStart = userInfoEnd + 1;
}
int16 hostEnd = fAuthority.FindFirst(':', hostAndPortStart);
if (hostEnd != B_ERROR) {
if (hostEnd < fAuthority.Length()-1) {
fPort = atoi(&(fAuthority.String())[hostEnd+1]);
fHasPort = true;
}
}
else
hostEnd = fAuthority.Length();
fAuthority.CopyInto(fHost, hostAndPortStart, hostEnd - hostAndPortStart);
SetHost(fHost);
}

View File

@ -64,6 +64,63 @@ void NetworkUrlTest::TestValidFullUrl()
}
void NetworkUrlTest::TestHostWithPathAndFragment()
{
BUrl url("http://1.2.3.4/some/path#frag/ment");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.Protocol() == "http");
CPPUNIT_ASSERT(url.HasProtocol());
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(url.Host() == "1.2.3.4");
CPPUNIT_ASSERT(url.HasHost());
CPPUNIT_ASSERT(!url.HasPort());
CPPUNIT_ASSERT(url.Path() == "/some/path");
CPPUNIT_ASSERT(url.HasPath());
CPPUNIT_ASSERT(!url.HasRequest());
CPPUNIT_ASSERT(url.Fragment() == "frag/ment");
CPPUNIT_ASSERT(url.HasFragment());
}
void NetworkUrlTest::TestHostWithFragment()
{
BUrl url("http://1.2.3.4#frag/ment");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.Protocol() == "http");
CPPUNIT_ASSERT(url.HasProtocol());
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(url.Host() == "1.2.3.4");
CPPUNIT_ASSERT(url.HasHost());
CPPUNIT_ASSERT(!url.HasPort());
CPPUNIT_ASSERT(url.HasPath()); // see Url.cpp - evidently an empty path is still a path?
CPPUNIT_ASSERT(!url.HasRequest());
CPPUNIT_ASSERT(url.Fragment() == "frag/ment");
CPPUNIT_ASSERT(url.HasFragment());
}
void NetworkUrlTest::TestIpv6HostPortPathAndRequest()
{
BUrl url("http://[123:123:0:123::123]:8080/some/path?key1=value1");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.Protocol() == "http");
CPPUNIT_ASSERT(url.HasProtocol());
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(url.Host() == "[123:123:0:123::123]");
CPPUNIT_ASSERT(url.HasHost());
CPPUNIT_ASSERT(url.Port() == 8080);
CPPUNIT_ASSERT(url.HasPort());
CPPUNIT_ASSERT(url.Path() == "/some/path");
CPPUNIT_ASSERT(url.HasPath());
CPPUNIT_ASSERT(url.Request() == "key1=value1");
CPPUNIT_ASSERT(url.HasRequest());
CPPUNIT_ASSERT(!url.HasFragment());
}
void NetworkUrlTest::TestFileUrl()
{
BUrl url("file:///northisland/wellington/brooklyn/windturbine");
@ -82,6 +139,21 @@ void NetworkUrlTest::TestFileUrl()
}
void NetworkUrlTest::TestDataUrl()
{
BUrl url("data:image/png;base64,iVBORw0KGI12P4//8/w38GIErkJggg==");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.Protocol() == "data");
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(!url.HasHost());
CPPUNIT_ASSERT(url.HasPath());
CPPUNIT_ASSERT(url.Path() == "image/png;base64,iVBORw0KGI12P4//8/w38GIErkJggg==");
CPPUNIT_ASSERT(!url.HasRequest());
CPPUNIT_ASSERT(!url.HasFragment());
}
// Authority Tests (UserName, Password, Host, Port) ----------------------------
@ -96,7 +168,7 @@ void NetworkUrlTest::TestWithUserNameAndPasswordNoHostAndPort()
CPPUNIT_ASSERT(url.Password() == "tree");
CPPUNIT_ASSERT(url.HasPassword());
CPPUNIT_ASSERT(url.Host() == "");
CPPUNIT_ASSERT(!url.HasHost());
CPPUNIT_ASSERT(url.HasHost()); // any authority means there "is a host" - see SetAuthority comment.
CPPUNIT_ASSERT(!url.HasPort());
CPPUNIT_ASSERT(url.Path() == "/x");
CPPUNIT_ASSERT(url.HasPath());
@ -194,6 +266,93 @@ void NetworkUrlTest::TestHostWithEmptyPort()
}
void NetworkUrlTest::TestProtocol()
{
BUrl url("olala:");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.Protocol() == "olala");
CPPUNIT_ASSERT(url.HasProtocol());
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(!url.HasHost());
CPPUNIT_ASSERT(!url.HasPort());
CPPUNIT_ASSERT(!url.HasPath());
CPPUNIT_ASSERT(!url.HasRequest());
CPPUNIT_ASSERT(!url.HasFragment());
}
void NetworkUrlTest::TestMailTo()
{
BUrl url("mailto:eric@example.com");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.Protocol() == "mailto");
CPPUNIT_ASSERT(url.HasProtocol());
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(!url.HasHost());
CPPUNIT_ASSERT(!url.HasPort());
CPPUNIT_ASSERT(url.Path() == "eric@example.com");
CPPUNIT_ASSERT(url.HasPath());
CPPUNIT_ASSERT(!url.HasRequest());
CPPUNIT_ASSERT(!url.HasFragment());
}
// Various Authority Checks ----------------------------------------------------
void NetworkUrlTest::TestAuthorityNoUserName()
{
BUrl url("anything://:pwd@host");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(url.HasPassword());
CPPUNIT_ASSERT(url.Password() == "pwd");
CPPUNIT_ASSERT(url.HasHost());
CPPUNIT_ASSERT(url.Host() == "host");
CPPUNIT_ASSERT(!url.HasPort());
}
void NetworkUrlTest::TestAuthorityWithCredentialsSeparatorNoPassword()
{
BUrl url("anything://unam:@host");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.HasUserName());
CPPUNIT_ASSERT(url.UserName() == "unam");
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(url.HasHost());
CPPUNIT_ASSERT(url.Host() == "host");
CPPUNIT_ASSERT(!url.HasPort());
}
void NetworkUrlTest::TestAuthorityWithoutCredentialsSeparatorNoPassword()
{
BUrl url("anything://unam@host");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(url.HasUserName());
CPPUNIT_ASSERT(url.UserName() == "unam");
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(url.HasHost());
CPPUNIT_ASSERT(url.Host() == "host");
CPPUNIT_ASSERT(!url.HasPort());
}
void NetworkUrlTest::TestAuthorityBadPort()
{
BUrl url("anything://host:aaa");
CPPUNIT_ASSERT(url.IsValid());
CPPUNIT_ASSERT(!url.HasUserName());
CPPUNIT_ASSERT(!url.HasPassword());
CPPUNIT_ASSERT(url.HasHost());
CPPUNIT_ASSERT(url.Host() == "host");
CPPUNIT_ASSERT(!url.HasPort());
}
// Invalid Forms ---------------------------------------------------------------
@ -225,6 +384,13 @@ void NetworkUrlTest::TestHttpNoHost()
}
void NetworkUrlTest::TestEmpty()
{
BUrl url("");
CPPUNIT_ASSERT(!url.IsValid());
}
// Control ---------------------------------------------------------------------
@ -251,6 +417,37 @@ NetworkUrlTest::AddTests(BTestSuite& parent)
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestHostWithEmptyPort",
&NetworkUrlTest::TestHostWithEmptyPort));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestHostWithPathAndFragment",
&NetworkUrlTest::TestHostWithPathAndFragment));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestHostWithFragment",
&NetworkUrlTest::TestHostWithFragment));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestIpv6HostPortPathAndRequest",
&NetworkUrlTest::TestIpv6HostPortPathAndRequest));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestProtocol",
&NetworkUrlTest::TestProtocol));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestMailTo",
&NetworkUrlTest::TestMailTo));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestDataUrl",
&NetworkUrlTest::TestDataUrl));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestAuthorityNoUserName",
&NetworkUrlTest::TestAuthorityNoUserName));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestAuthorityWithCredentialsSeparatorNoPassword",
&NetworkUrlTest::TestAuthorityWithCredentialsSeparatorNoPassword));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestAuthorityWithoutCredentialsSeparatorNoPassword",
&NetworkUrlTest::TestAuthorityWithoutCredentialsSeparatorNoPassword));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestAuthorityBadPort",
&NetworkUrlTest::TestAuthorityBadPort));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestWhitespaceBefore",
@ -261,6 +458,9 @@ NetworkUrlTest::AddTests(BTestSuite& parent)
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestWhitespaceMiddle",
&NetworkUrlTest::TestWhitespaceMiddle));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestEmpty",
&NetworkUrlTest::TestEmpty));
suite.addTest(new CppUnit::TestCaller<NetworkUrlTest>(
"NetworkUrlTest::TestFileUrl",

View File

@ -28,11 +28,23 @@ public:
void TestHostWithNoPortNoPath();
void TestHostWithPortNoPath();
void TestHostWithEmptyPort();
void TestHostWithPathAndFragment();
void TestHostWithFragment();
void TestIpv6HostPortPathAndRequest();
void TestProtocol();
void TestMailTo();
void TestDataUrl();
void TestAuthorityNoUserName();
void TestAuthorityWithCredentialsSeparatorNoPassword();
void TestAuthorityWithoutCredentialsSeparatorNoPassword();
void TestAuthorityBadPort();
void TestWhitespaceBefore();
void TestWhitespaceAfter();
void TestWhitespaceMiddle();
void TestHttpNoHost();
void TestEmpty();
static void AddTests(BTestSuite& suite);