This is an automated email from the ASF dual-hosted git repository.
Mryange pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new f4428595711 [fix](function) Align parse_url boundaries and nullable
execution (#68490)
f4428595711 is described below
commit f442859571142f44196896a6f3e6a4014b1b30fa
Author: Mryange <[email protected]>
AuthorDate: Thu Oct 8 11:47:12 2026 +0800
[fix](function) Align parse_url boundaries and nullable execution (#68490)
`parse_url` treated delimiters in the path, query, or fragment as
authority separators, producing incorrect `HOST`, `PORT`, `USERINFO`,
`PATH`, `FILE`, and `QUERY` results. The BE implementation also assumed
non-null, non-const string columns and could materialize nullable
arguments incorrectly. This change bounds parsing to the correct URL
components, supports bracketed IPv6 hosts, and evaluates nullable and
const arguments row by row in BE while keeping FE constant folding
aligned.
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [ ] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [ ] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
be/src/exprs/function/function_string_url.cpp | 148 ++++++++-------------
be/src/util/url_parser.cpp | 95 ++++++++-----
be/test/exprs/function/function_url_test.cpp | 17 +++
.../functions/executable/StringArithmetic.java | 55 ++++++--
.../functions/executable/StringArithmeticTest.java | 7 +
5 files changed, 186 insertions(+), 136 deletions(-)
diff --git a/be/src/exprs/function/function_string_url.cpp
b/be/src/exprs/function/function_string_url.cpp
index 3592356f98d..dcdaa6fccf7 100644
--- a/be/src/exprs/function/function_string_url.cpp
+++ b/be/src/exprs/function/function_string_url.cpp
@@ -18,13 +18,13 @@
#include <cstddef>
#include <string>
#include <string_view>
-#include <vector>
#include "common/status.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/block/column_numbers.h"
#include "core/column/column_const.h"
+#include "core/column/column_execute_util.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_vector.h"
@@ -32,7 +32,6 @@
#include "core/data_type/data_type_string.h"
#include "core/string_ref.h"
#include "exec/common/stringop_substring.h"
-#include "exec/common/template_helpers.hpp"
#include "exprs/function/function.h"
#include "exprs/function/function_helpers.h"
#include "exprs/function/simple_function_factory.h"
@@ -93,6 +92,7 @@ public:
String get_name() const override { return name; }
size_t get_number_of_arguments() const override { return 0; }
bool is_variadic() const override { return true; }
+ bool use_default_implementation_for_nulls() const override { return false;
}
DataTypePtr get_return_type_impl(const DataTypes& arguments) const
override {
return make_nullable(std::make_shared<DataTypeString>());
@@ -111,101 +111,67 @@ public:
size_t argument_size = arguments.size();
const bool has_key = argument_size == 3;
- std::vector<ColumnPtr> argument_columns(argument_size);
- std::vector<UInt8> col_const(argument_size);
- for (size_t i = 0; i < argument_size; ++i) {
- std::tie(argument_columns[i], col_const[i]) =
-
unpack_if_const(block.get_by_position(arguments[i]).column);
- }
-
- const auto* url_col = assert_cast<const
ColumnString*>(argument_columns[0].get());
- const auto* part_col = assert_cast<const
ColumnString*>(argument_columns[1].get());
- const bool part_const = col_const[1];
- std::vector<UrlParser::UrlPart> url_parts;
- const int part_nums = part_const ? 1 : input_rows_count;
-
- url_parts.resize(part_nums);
- for (int i = 0; i < part_nums; i++) {
- StringRef part = part_col->get_data_at(i);
- UrlParser::UrlPart url_part = UrlParser::get_url_part(part);
- if (url_part == UrlParser::INVALID) {
- return Status::RuntimeError("Invalid URL part: {}\n{}",
- std::string(part.data, part.size),
- "(Valid URL parts are 'PROTOCOL',
'HOST', "
- "'PATH', 'REF', 'AUTHORITY', "
- "'FILE', 'USERINFO', 'PORT' and
'QUERY')");
- }
- url_parts[i] = url_part;
- }
-
+ const auto url_col =
+
ColumnView<TYPE_STRING>::create(block.get_by_position(arguments[0]).column);
+ const auto part_col =
+
ColumnView<TYPE_STRING>::create(block.get_by_position(arguments[1]).column);
if (has_key) {
- const bool url_const = col_const[0];
- const bool key_const = col_const[2];
- const auto* key_col = assert_cast<const
ColumnString*>(argument_columns[2].get());
- RETURN_IF_ERROR(std::visit(
- [&](auto url_const, auto part_const, auto key_const) {
- return vector_parse_key<url_const, part_const,
key_const>(
- url_col, url_parts, key_col, input_rows_count,
null_map_data,
- res_chars, res_offsets);
- },
- make_bool_variant(url_const),
make_bool_variant(part_const),
- make_bool_variant(key_const)));
- } else {
- const bool url_const = col_const[0];
- RETURN_IF_ERROR(std::visit(
- [&](auto url_const, auto part_const) {
- return vector_parse<url_const, part_const>(url_col,
url_parts,
-
input_rows_count, null_map_data,
- res_chars,
res_offsets);
- },
- make_bool_variant(url_const),
make_bool_variant(part_const)));
- }
- block.get_by_position(result).column =
- ColumnNullable::create(std::move(res), std::move(null_map));
- return Status::OK();
- }
- template <bool url_const, bool part_const>
- static Status vector_parse(const ColumnString* url_col,
- std::vector<UrlParser::UrlPart>& url_parts,
const int size,
- ColumnUInt8::Container& null_map_data,
- ColumnString::Chars& res_chars,
ColumnString::Offsets& res_offsets) {
- for (size_t i = 0; i < size; ++i) {
- UrlParser::UrlPart& url_part =
url_parts[index_check_const<part_const>(i)];
- StringRef url_val =
url_col->get_data_at(index_check_const<url_const>(i));
- StringRef parse_res;
- if (UrlParser::parse_url(url_val, url_part, &parse_res)) {
- if (parse_res.empty()) [[unlikely]] {
- StringOP::push_empty_string(i, res_chars, res_offsets);
+ const auto key_col =
+
ColumnView<TYPE_STRING>::create(block.get_by_position(arguments[2]).column);
+ for (size_t i = 0; i < input_rows_count; ++i) {
+ if (url_col.is_null_at(i) || part_col.is_null_at(i) ||
key_col.is_null_at(i)) {
+ StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
continue;
}
- StringOP::push_value_string(std::string_view(parse_res.data,
parse_res.size), i,
- res_chars, res_offsets);
- } else {
- StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
+ const auto part = part_col.value_at(i);
+ const auto url_part = UrlParser::get_url_part(part);
+ if (url_part == UrlParser::INVALID) {
+ return Status::RuntimeError("Invalid URL part: {}\n{}",
+ std::string(part.data,
part.size),
+ "(Valid URL parts are
'PROTOCOL', 'HOST', "
+ "'PATH', 'REF', 'AUTHORITY', "
+ "'FILE', 'USERINFO', 'PORT'
and 'QUERY')");
+ }
+ StringRef parse_res;
+ if (UrlParser::parse_url_key(url_col.value_at(i), url_part,
key_col.value_at(i),
+ &parse_res)) {
+
StringOP::push_value_string(std::string_view(parse_res.data, parse_res.size), i,
+ res_chars, res_offsets);
+ } else {
+ StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
+ }
}
- }
- return Status::OK();
- }
- template <bool url_const, bool part_const, bool key_const>
- static Status vector_parse_key(const ColumnString* url_col,
- std::vector<UrlParser::UrlPart>& url_parts,
- const ColumnString* key_col, const int size,
- ColumnUInt8::Container& null_map_data,
- ColumnString::Chars& res_chars,
- ColumnString::Offsets& res_offsets) {
- for (size_t i = 0; i < size; ++i) {
- UrlParser::UrlPart& url_part =
url_parts[index_check_const<part_const>(i)];
- StringRef url_val =
url_col->get_data_at(index_check_const<url_const>(i));
- StringRef url_key =
key_col->get_data_at(index_check_const<key_const>(i));
- StringRef parse_res;
- if (UrlParser::parse_url_key(url_val, url_part, url_key,
&parse_res)) {
- StringOP::push_value_string(std::string_view(parse_res.data,
parse_res.size), i,
- res_chars, res_offsets);
- } else {
- StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
- continue;
+ } else {
+ for (size_t i = 0; i < input_rows_count; ++i) {
+ if (url_col.is_null_at(i) || part_col.is_null_at(i)) {
+ StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
+ continue;
+ }
+ const auto part = part_col.value_at(i);
+ const auto url_part = UrlParser::get_url_part(part);
+ if (url_part == UrlParser::INVALID) {
+ return Status::RuntimeError("Invalid URL part: {}\n{}",
+ std::string(part.data,
part.size),
+ "(Valid URL parts are
'PROTOCOL', 'HOST', "
+ "'PATH', 'REF', 'AUTHORITY', "
+ "'FILE', 'USERINFO', 'PORT'
and 'QUERY')");
+ }
+ StringRef parse_res;
+ if (UrlParser::parse_url(url_col.value_at(i), url_part,
&parse_res)) {
+ if (parse_res.empty()) {
+ StringOP::push_empty_string(i, res_chars, res_offsets);
+ } else {
+ StringOP::push_value_string(
+ std::string_view(parse_res.data,
parse_res.size), i, res_chars,
+ res_offsets);
+ }
+ } else {
+ StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
+ }
}
}
+ block.get_by_position(result).column =
+ ColumnNullable::create(std::move(res), std::move(null_map));
return Status::OK();
}
};
diff --git a/be/src/util/url_parser.cpp b/be/src/util/url_parser.cpp
index 0f44757e442..29cc93ce4f5 100644
--- a/be/src/util/url_parser.cpp
+++ b/be/src/util/url_parser.cpp
@@ -111,10 +111,15 @@ bool UrlParser::parse_url(const StringRef& url, UrlPart
part, StringRef* result)
case FILE:
case PATH: {
- // Find first '/'.
- int32_t start_pos = _s_slash_search.search(&protocol_end);
-
- if (start_pos < 0) {
+ int32_t slash_pos = _s_slash_search.search(&protocol_end);
+ int32_t question_pos = _s_question_search.search(&protocol_end);
+ int32_t hash_pos = _s_hash_search.search(&protocol_end);
+ int32_t start_pos = slash_pos;
+ if (part == FILE && (start_pos < 0 || (question_pos >= 0 &&
question_pos < start_pos))) {
+ start_pos = question_pos;
+ }
+ if (start_pos < 0 || (part == PATH && question_pos >= 0 &&
question_pos < start_pos) ||
+ (hash_pos >= 0 && hash_pos < start_pos)) {
// Return empty string. This is what Hive does.
return true;
}
@@ -128,10 +133,9 @@ bool UrlParser::parse_url(const StringRef& url, UrlPart
part, StringRef* result)
} else {
// End string _s_at next '?' or '#'.
end_pos = _s_question_search.search(&path_start);
-
- if (end_pos < 0) {
- // No '?' was found, look for '#'.
- end_pos = _s_hash_search.search(&path_start);
+ int32_t path_hash_pos = _s_hash_search.search(&path_start);
+ if (end_pos < 0 || (path_hash_pos >= 0 && path_hash_pos <
end_pos)) {
+ end_pos = path_hash_pos;
}
}
@@ -141,20 +145,28 @@ bool UrlParser::parse_url(const StringRef& url, UrlPart
part, StringRef* result)
case HOST: {
StringRef authority = find_authority(protocol_end);
- // Find '@' to strip out the userinfo.
- int32_t start_pos = _s_at_search.search(&authority);
+ int32_t userinfo_end = -1;
+ for (int32_t i = 0; i < authority.size; ++i) {
+ if (authority.data[i] == '@') {
+ userinfo_end = i;
+ }
+ }
- if (start_pos < 0) {
- // No '@' was found, i.e., no user:pass info was given.
- start_pos = 0;
+ StringRef host_start = authority.substring(userinfo_end + 1);
+ int32_t end_pos = cast_set<int32_t>(host_start.size);
+ if (!host_start.empty() && host_start.data[0] == '[') {
+ for (int32_t i = 1; i < host_start.size; ++i) {
+ if (host_start.data[i] == ']') {
+ end_pos = i + 1;
+ break;
+ }
+ }
} else {
- // Skip '@'.
- start_pos += _s_at.size;
+ int32_t colon_pos = _s_colon_search.search(&host_start);
+ if (colon_pos >= 0) {
+ end_pos = colon_pos;
+ }
}
-
- StringRef host_start = authority.substring(start_pos);
- // Find ':' to strip out port.
- int32_t end_pos = _s_colon_search.search(&host_start);
*result = host_start.substring(0, end_pos);
break;
}
@@ -192,8 +204,12 @@ bool UrlParser::parse_url(const StringRef& url, UrlPart
part, StringRef* result)
case USERINFO: {
StringRef authority = find_authority(protocol_end);
- // Find '@'.
- int32_t end_pos = _s_at_search.search(&authority);
+ int32_t end_pos = -1;
+ for (int32_t i = 0; i < authority.size; ++i) {
+ if (authority.data[i] == '@') {
+ end_pos = i;
+ }
+ }
if (end_pos < 0) {
// Indicate no user and pass were given.
@@ -206,26 +222,35 @@ bool UrlParser::parse_url(const StringRef& url, UrlPart
part, StringRef* result)
case PORT: {
StringRef authority = find_authority(protocol_end);
- // Find '@' to strip out the userinfo.
- int32_t start_pos = _s_at_search.search(&authority);
+ int32_t userinfo_end = -1;
+ for (int32_t i = 0; i < authority.size; ++i) {
+ if (authority.data[i] == '@') {
+ userinfo_end = i;
+ }
+ }
- if (start_pos < 0) {
- // No '@' was found, i.e., no user:pass info was given.
- start_pos = 0;
+ StringRef host_start = authority.substring(userinfo_end + 1);
+ int32_t port_start = -1;
+ if (!host_start.empty() && host_start.data[0] == '[') {
+ for (int32_t i = 1; i < host_start.size; ++i) {
+ if (host_start.data[i] == ']') {
+ if (i + 1 < host_start.size && host_start.data[i + 1] ==
':') {
+ port_start = i + 2;
+ }
+ break;
+ }
+ }
} else {
- // Skip '@'.
- start_pos += _s_at.size;
+ int32_t colon_pos = _s_colon_search.search(&host_start);
+ if (colon_pos >= 0) {
+ port_start = colon_pos + 1;
+ }
}
-
- StringRef host_start = authority.substring(start_pos);
- // Find ':' to strip out port.
- int32_t end_pos = _s_colon_search.search(&host_start);
- //no port found
- if (end_pos < 0) {
+ if (port_start < 0) {
return false;
}
- *result = host_start.substring(end_pos + _s_colon.size);
+ *result = host_start.substring(port_start);
break;
}
diff --git a/be/test/exprs/function/function_url_test.cpp
b/be/test/exprs/function/function_url_test.cpp
index 5ede3ee9821..263346170c7 100644
--- a/be/test/exprs/function/function_url_test.cpp
+++ b/be/test/exprs/function/function_url_test.cpp
@@ -99,6 +99,10 @@ TEST(FunctionUrlTEST, ParseUrlQueryKeyTest) {
PrimitiveType::TYPE_VARCHAR};
DataSet data_set = {
+ // Every nullable argument must short-circuit independently in the
keyed form.
+ {{Null(), STRING("QUERY"), STRING("k")}, Null()},
+ {{STRING("http://h/p?k=1"), Null(), STRING("k")}, Null()},
+ {{STRING("http://h/p?k=1"), STRING("QUERY"), Null()}, Null()},
// The only '?' is inside the fragment, so the url has no query
component.
{{STRING("http://h/p#f?k=v"), STRING("QUERY"), STRING("k")},
Null()},
// The '#' comes before the '?', so it is a fragment instead of a
query.
@@ -166,6 +170,11 @@ TEST(FunctionUrlTEST, ParseUrlAuthorityTest) {
InputTypeSet input_types = {PrimitiveType::TYPE_VARCHAR,
PrimitiveType::TYPE_VARCHAR};
DataSet data_set = {
+ // A nullable dynamic part must be handled row by row. A NULL part
must not be
+ // materialized as an invalid empty part while another row is
being parsed.
+ {{Null(), STRING("HOST")}, Null()},
+ {{STRING("http://example.com/path?x=1"), STRING("HOST")},
STRING("example.com")},
+ {{STRING("http://example.com/path?x=1"), Null()}, Null()},
// A ':' in the path is not a port separator, and an '@' in the
path is not a
// userinfo separator.
{{STRING("http://example.com/a:b"), STRING("HOST")},
STRING("example.com")},
@@ -178,6 +187,14 @@ TEST(FunctionUrlTEST, ParseUrlAuthorityTest) {
{{STRING("http://example.com#f:1"), STRING("HOST")},
STRING("example.com")},
{{STRING("http://example.com#f:1"), STRING("PORT")}, Null()},
{{STRING("http://example.com?x=1"), STRING("AUTHORITY")},
STRING("example.com")},
+ {{STRING("http://[2001:db8::1]:8080/a?x=1#r"), STRING("HOST")},
+ STRING("[2001:db8::1]")},
+ {{STRING("http://[2001:db8::1]:8080/a?x=1#r"), STRING("PORT")},
STRING("8080")},
+ {{STRING("http://h/p#frag?x=1"), STRING("PATH")}, STRING("/p")},
+ {{STRING("http://h/p#frag?x=1"), STRING("QUERY")}, Null()},
+ // FILE includes a query even when the URL has no path, while PATH
remains empty.
+ {{STRING("http://h?k=/v#frag"), STRING("FILE")}, STRING("?k=/v")},
+ {{STRING("http://h?k=/v#frag"), STRING("PATH")}, STRING("")},
// A real port and a real userinfo are still returned.
{{STRING("http://user:[email protected]:80/a:b"), STRING("HOST")},
STRING("example.com")},
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
index 04c737197ed..f1ab9c1a676 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
@@ -992,6 +992,21 @@ public class StringArithmetic {
return Math.min(firstIndex, secondIndex);
}
+ private static int firstIndexOf(String value, char first, char second,
char third) {
+ return firstIndexOf(value, firstIndexOf(value, first, second), third);
+ }
+
+ private static int firstIndexOf(String value, int firstIndex, char second)
{
+ int secondIndex = value.indexOf(second);
+ if (firstIndex < 0) {
+ return secondIndex;
+ }
+ if (secondIndex < 0) {
+ return firstIndex;
+ }
+ return Math.min(firstIndex, secondIndex);
+ }
+
private static String substringEnd(String value, int end) {
return end < 0 ? value : value.substring(0, end);
}
@@ -1008,8 +1023,8 @@ public class StringArithmetic {
}
private static String parseUrlPath(String protocolEnd) {
- int startPos = protocolEnd.indexOf('/');
- if (startPos < 0) {
+ int startPos = firstIndexOf(protocolEnd, '/', '?', '#');
+ if (startPos < 0 || protocolEnd.charAt(startPos) != '/') {
return "";
}
String pathStart = protocolEnd.substring(startPos);
@@ -1018,7 +1033,12 @@ public class StringArithmetic {
private static String parseUrlFile(String protocolEnd) {
int startPos = protocolEnd.indexOf('/');
- if (startPos < 0) {
+ int queryPos = protocolEnd.indexOf('?');
+ int fragmentPos = protocolEnd.indexOf('#');
+ if (startPos < 0 || (queryPos >= 0 && queryPos < startPos)) {
+ startPos = queryPos;
+ }
+ if (startPos < 0 || (fragmentPos >= 0 && fragmentPos < startPos)) {
return "";
}
String pathStart = protocolEnd.substring(startPos);
@@ -1027,9 +1047,13 @@ public class StringArithmetic {
private static String parseUrlHost(String protocolEnd) {
String authority = parseUrlAuthority(protocolEnd);
- int startPos = authority.indexOf('@');
+ int startPos = authority.lastIndexOf('@');
startPos = startPos < 0 ? 0 : startPos + 1;
String hostStart = authority.substring(startPos);
+ if (hostStart.startsWith("[")) {
+ int closeBracket = hostStart.indexOf(']');
+ return closeBracket < 0 ? hostStart : hostStart.substring(0,
closeBracket + 1);
+ }
return substringEnd(hostStart, hostStart.indexOf(':'));
}
@@ -1058,7 +1082,7 @@ public class StringArithmetic {
private static String parseUrlUserInfo(String protocolEnd) {
String authority = parseUrlAuthority(protocolEnd);
- int endPos = authority.indexOf('@');
+ int endPos = authority.lastIndexOf('@');
if (endPos < 0) {
return null;
}
@@ -1067,14 +1091,25 @@ public class StringArithmetic {
private static String parseUrlPort(String protocolEnd) {
String authority = parseUrlAuthority(protocolEnd);
- int startPos = authority.indexOf('@');
+ int startPos = authority.lastIndexOf('@');
startPos = startPos < 0 ? 0 : startPos + 1;
String hostStart = authority.substring(startPos);
- int endPos = hostStart.indexOf(':');
- if (endPos < 0) {
- return null;
+ int portStart;
+ if (hostStart.startsWith("[")) {
+ int closeBracket = hostStart.indexOf(']');
+ if (closeBracket < 0 || closeBracket + 1 >= hostStart.length()
+ || hostStart.charAt(closeBracket + 1) != ':') {
+ return null;
+ }
+ portStart = closeBracket + 2;
+ } else {
+ int colonPos = hostStart.indexOf(':');
+ if (colonPos < 0) {
+ return null;
+ }
+ portStart = colonPos + 1;
}
- return hostStart.substring(endPos + 1);
+ return hostStart.substring(portStart);
}
/**
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
index ba23bd11027..22f0292f7c9 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
@@ -134,6 +134,13 @@ class StringArithmeticTest {
assertParseUrl("http://example.com#f:1", "HOST", "example.com");
assertParseUrlIsNull("http://example.com#f:1", "PORT");
assertParseUrl("http://example.com?x=1", "AUTHORITY", "example.com");
+ assertParseUrl("http://[2001:db8::1]:8080/a?x=1#r", "HOST",
"[2001:db8::1]");
+ assertParseUrl("http://[2001:db8::1]:8080/a?x=1#r", "PORT", "8080");
+ assertParseUrl("http://h/p#frag?x=1", "PATH", "/p");
+ assertParseUrlIsNull("http://h/p#frag?x=1", "QUERY");
+ // FILE includes a query even when the URL has no path, while PATH
remains empty.
+ assertParseUrl("http://h?k=/v#frag", "FILE", "?k=/v");
+ assertParseUrl("http://h?k=/v#frag", "PATH", "");
// A real port and a real userinfo are still returned.
assertParseUrl("http://user:[email protected]:80/a:b", "HOST",
"example.com");
assertParseUrl("http://user:[email protected]:80/a:b", "PORT", "80");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]