Commit c03dd9b0 authored by Dominik Charousset's avatar Dominik Charousset

Add percent encoding to URI

parent 1cada153
......@@ -105,6 +105,10 @@ void read_uri_query(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
fin();
}
#define read_next_char(next_state) \
transition(next_state, unprotected_char, str += ch) \
fsm_transition(read_uri_percent_encoded(ps, str), next_state, '%')
template <class Iterator, class Sentinel, class Consumer>
void read_uri(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
// Local variables.
......@@ -122,8 +126,8 @@ void read_uri(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
auto path_char = [](char c) {
return in_whitelist(alphanumeric_chars, c) || c == '/';
};
auto authority_char = [](char c) {
return in_whitelist(alphanumeric_chars, c) || c == '.';
auto unprotected_char = [](char c) {
return in_whitelist(alphanumeric_chars, c) || in_whitelist("-._~", c);
};
// Utility setters for avoiding code duplication.
auto set_path = [&] {
......@@ -148,13 +152,12 @@ void read_uri(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
epsilon(read_scheme)
}
state(read_scheme) {
fsm_transition(read_uri_percent_encoded(ps, str), read_scheme, '%')
transition(read_scheme, alphabetic_chars, str += ch)
read_next_char(read_scheme)
transition(have_scheme, ':', consumer.scheme(take_str()))
}
state(have_scheme) {
transition(disambiguate_path, '/')
transition(read_path, alphanumeric_chars, str += ch)
read_next_char(read_path)
fsm_transition(read_uri_percent_encoded(ps, str), read_path, '%')
}
// This state is terminal, because "file:/" is a valid URI.
......@@ -163,28 +166,32 @@ void read_uri(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
epsilon(read_path, any_char, str += '/')
}
state(start_authority) {
transition(read_authority, authority_char, str += ch)
fsm_transition(read_ipv6_address(ps, ip_consumer), end_of_ipv6_host, '[')
read_next_char(read_authority)
fsm_transition(read_ipv6_address(ps, ip_consumer), await_end_of_ipv6, '[')
}
state(await_end_of_ipv6) {
transition(end_of_ipv6_host, ']')
}
state(end_of_ipv6_host) {
transition(end_of_host, ']')
term_state(end_of_ipv6_host) {
transition(start_port, ':')
epsilon(end_of_authority)
}
term_state(end_of_host) {
transition(start_port, ':', set_host())
epsilon(end_of_authority, "/?#", set_host())
}
term_state(read_authority, set_host()) {
transition(read_authority, authority_char, str += ch)
read_next_char(read_authority)
transition(start_host, '@', set_userinfo())
transition(start_port, ':', set_host())
epsilon(end_of_authority, "/?#", set_host())
}
state(start_host) {
transition(read_host, authority_char, str += ch)
fsm_transition(read_ipv6_address(ps, ip_consumer), end_of_ipv6_host, '[')
read_next_char(read_host)
fsm_transition(read_ipv6_address(ps, ip_consumer), await_end_of_ipv6, '[')
}
term_state(read_host, set_host()) {
transition(read_host, authority_char, str += ch)
read_next_char(read_host)
transition(start_port, ':', set_host())
epsilon(end_of_authority, "/?#", set_host())
}
......@@ -194,7 +201,7 @@ void read_uri(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
term_state(read_port, consumer.port(port)) {
transition(read_port, decimal_chars, add_ascii<10>(port, ch),
pec::integer_overflow)
epsilon(end_of_authority, "/?#", set_host())
epsilon(end_of_authority, "/?#")
}
term_state(end_of_authority) {
transition(read_path, '/')
......@@ -203,6 +210,7 @@ void read_uri(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
}
term_state(read_path, set_path()) {
transition(read_path, path_char, str += ch)
fsm_transition(read_uri_percent_encoded(ps, str), read_path, '%')
transition(start_query, '?', set_path())
transition(read_fragment, '#', set_path())
}
......@@ -214,7 +222,7 @@ void read_uri(state<Iterator, Sentinel>& ps, Consumer&& consumer) {
epsilon(done)
}
term_state(read_fragment, consumer.fragment(take_str())) {
transition(read_fragment, alphanumeric_chars, str += ch)
read_next_char(read_fragment)
}
term_state(done) {
// nop
......
......@@ -68,6 +68,10 @@ public:
/// Assembles the human-readable string representation for this URI.
void assemble_str();
// Escapes all reserved characters according to RFC 3986 in `x` and
// adds the encoded string to `str`.
void add_encoded(string_view x, bool is_path = false);
// -- friend functions -------------------------------------------------------
friend void intrusive_ptr_add_ref(const uri_impl* p);
......
......@@ -53,6 +53,13 @@ public:
inline authority_type() : port(0) {
// nop
}
/// Returns whether `host` is empty, i.e., the host is not an IP address
/// and the string is empty.
bool empty() const noexcept {
auto str = get_if<std::string>(&host);
return str != nullptr && str->empty();
}
};
/// Separates the query component into key-value pairs.
......@@ -110,12 +117,6 @@ private:
// -- related free functions ---------------------------------------------------
/// @relates uri
std::string to_string(const uri::host_type& x);
/// @relates uri
std::string to_string(const uri::authority_type& x);
/// @relates uri
std::string to_string(const uri& x);
......
......@@ -74,33 +74,6 @@ int uri::compare(string_view x) const noexcept {
// -- related free functions ---------------------------------------------------
std::string to_string(const uri::host_type& x) {
auto str = get_if<std::string>(&x);
if (str)
return *str;
auto addr = get<ip_address>(x);
if (addr.embeds_v4())
return to_string(addr.embedded_v4());
std::string result = to_string(addr);
result.insert(result.begin(), '[');
result += ']';
return result;
}
std::string to_string(const uri::authority_type& x) {
std::string result;
if (!x.userinfo.empty()) {
result += x.userinfo;
result += '@';
}
result += to_string(x.host);
if (x.port != 0) {
result += ':';
result += std::to_string(x.port);
}
return result;
}
std::string to_string(const uri& x) {
auto x_str = x.str();
std::string result{x_str.begin(), x_str.end()};
......
......@@ -39,37 +39,87 @@ uri_impl uri_impl::default_instance;
// -- modifiers ----------------------------------------------------------------
void uri_impl::assemble_str() {
// TODO: all substrings need percent-escaping
str = scheme;
add_encoded(scheme);
str += ':';
auto authority_str = to_string(authority);
if (!authority_str.empty()) {
if (authority.empty()) {
CAF_ASSERT(!path.empty());
add_encoded(path, true);
} else {
str += "//";
str += authority_str;
if (!authority.userinfo.empty()) {
add_encoded(authority.userinfo);
str += '@';
}
auto addr = get_if<ip_address>(&authority.host);
if (addr == nullptr) {
add_encoded(get<std::string>(authority.host));
} else {
str += '[';
str += to_string(*addr);
str += ']';
}
if (authority.port != 0) {
str += ':';
str += std::to_string(authority.port);
}
if (!path.empty()) {
str += '/';
str += path;
addr += '/';
add_encoded(path, true);
}
} else if (!path.empty()) {
str += path;
}
if (!query.empty()) {
str += '?';
auto i = query.begin();
str += i->first;
auto add_kvp = [&](decltype(*i) kvp) {
add_encoded(kvp.first);
str += '=';
str += i->second;
add_encoded(kvp.second);
};
add_kvp(*i);
++i;
for (; i != query.end(); ++i) {
str += '&';
str += i->first;
str += '=';
str += i->second;
add_kvp(*i);
}
}
if (!fragment.empty()) {
str += '#';
str += fragment;
add_encoded(fragment);
}
}
void uri_impl::add_encoded(string_view x, bool is_path) {
for (auto ch : x)
switch (ch) {
case '/':
if (is_path) {
str += ch;
break;
}
case ' ':
case ':':
case '?':
case '#':
case '[':
case ']':
case '@':
case '!':
case '$':
case '&':
case '\'':
case '"':
case '(':
case ')':
case '*':
case '+':
case ',':
case ';':
case '=':
str += '%';
detail::append_hex(str, reinterpret_cast<uint8_t*>(&ch), 1);
break;
default:
str += ch;
}
}
......
......@@ -202,6 +202,7 @@ CAF_TEST(constructing) {
CAF_CHECK_EQUAL(*(http << components), *(http_str << components))
CAF_TEST(builder construction) {
// all combinations of components
BUILD(file);
BUILD(file << kvp);
BUILD(file << frag);
......@@ -238,11 +239,21 @@ CAF_TEST(builder construction) {
BUILD(me << node << port80 << file << frag);
BUILD(me << node << port80 << file << kvp);
BUILD(me << node << port80 << file << kvp << frag);
// percent encoding
auto escaped = uri_builder{}
.scheme("hi there")
.userinfo("it's")
.host("me/")
.path("file 1")
.fragment("[42]")
.make();
CAF_CHECK_EQUAL(escaped, "hi%20there://it%27s@me%21/file%201#%5B42%5D");
}
#define ROUNDTRIP(str) CAF_CHECK_EQUAL(str##_u, str)
CAF_TEST(from string) {
// all combinations of components
ROUNDTRIP("http:file");
ROUNDTRIP("http:file?a=1&b=2");
ROUNDTRIP("http:file#42");
......@@ -279,6 +290,7 @@ CAF_TEST(from string) {
ROUNDTRIP("http://me@node:80/file?a=1&b=2");
ROUNDTRIP("http://me@node:80/file#42");
ROUNDTRIP("http://me@node:80/file?a=1&b=2#42");
// all combinations of with IPv6 host
ROUNDTRIP("http://[::1]");
ROUNDTRIP("http://[::1]?a=1&b=2");
ROUNDTRIP("http://[::1]#42");
......@@ -311,6 +323,8 @@ CAF_TEST(from string) {
ROUNDTRIP("http://me@[::1]:80/file?a=1&b=2");
ROUNDTRIP("http://me@[::1]:80/file#42");
ROUNDTRIP("http://me@[::1]:80/file?a=1&b=2#42");
// percent encoding
ROUNDTRIP("hi%20there://it%27s@me%21/file%201#%5B42%5D");
}
CAF_TEST(empty components) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment