Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/core/uri/escape.cc
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,35 @@ auto URI::unescape(const std::string_view input) -> std::string {
return result;
}

auto URI::unescape_form(const std::string_view input, std::string &output)
-> bool {
const auto base{output.size()};
output.reserve(base + input.size());
for (std::size_t position = 0; position < input.size();) {
const auto character{input[position]};
if (character == '+') {
output += ' ';
position += 1;
} else if (character == URI_PERCENT) {
const auto high{position + 2 < input.size()
? hex_digit_value(input[position + 1])
: static_cast<std::int8_t>(-1)};
const auto low{high < 0 ? static_cast<std::int8_t>(-1)
: hex_digit_value(input[position + 2])};
if (low < 0) {
output.resize(base);
return false;
}

output += static_cast<char>((high << 4) | low);
position += 3;
} else {
output += character;
position += 1;
}
}

return true;
}

} // namespace sourcemeta::core
20 changes: 20 additions & 0 deletions src/core/uri/include/sourcemeta/core/uri.h
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,26 @@ class SOURCEMETA_CORE_URI_EXPORT URI {
/// ```
[[nodiscard]] static auto unescape(std::string_view input) -> std::string;

/// Decode an "application/x-www-form-urlencoded" component (RFC 6749 Appendix
/// B and the HTML URL-encoded form syntax), appending the decoded bytes to
/// the output. Each "+" becomes a space and each "%" followed by two
/// hexadecimal digits becomes its octet. A "%" that is not followed by two
/// hexadecimal digits is rejected, returning false with the output restored
/// to its original contents. The output must not alias the input. For
/// example:
///
/// ```cpp
/// #include <sourcemeta/core/uri.h>
/// #include <cassert>
/// #include <string>
///
/// std::string output;
/// assert(sourcemeta::core::URI::unescape_form("a+b%2Fc", output));
/// assert(output == "a b/c");
/// ```
[[nodiscard]] static auto unescape_form(std::string_view input,
std::string &output) -> bool;

/// Remove the "." and ".." segments from a URI path per RFC 3986 Section
/// 5.2.4, preserving leading ".." segments in a relative path. For example:
///
Expand Down
1 change: 1 addition & 0 deletions test/uri/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME uri
uri_canonicalize_test.cc
uri_escape_test.cc
uri_unescape_test.cc
uri_unescape_form_test.cc
uri_normalize_path_test.cc
uri_resolve_from_test.cc
uri_relative_to_test.cc
Expand Down
115 changes: 115 additions & 0 deletions test/uri/uri_unescape_form_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#include <sourcemeta/core/test.h>
#include <sourcemeta/core/uri.h>

#include <string> // std::string

TEST(plain_value_passes_through) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("foobar", output));
EXPECT_EQ(output, "foobar");
}

TEST(plus_becomes_space) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("foo+bar", output));
EXPECT_EQ(output, "foo bar");
}

TEST(percent_twenty_becomes_space) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("foo%20bar", output));
EXPECT_EQ(output, "foo bar");
}

TEST(percent_two_f_becomes_slash) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("path%2Fto", output));
EXPECT_EQ(output, "path/to");
}

TEST(uppercase_hex) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("%C3%A9", output));
EXPECT_EQ(output, "\xc3\xa9");
}

TEST(lowercase_hex) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("%c3%a9", output));
EXPECT_EQ(output, "\xc3\xa9");
}

TEST(encoded_plus_stays_a_plus) {
// "%2B" is a literal "+", distinct from a "+" that means a space, which is
// the round trip the plain percent decoder cannot preserve
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("a%2Bb", output));
EXPECT_EQ(output, "a+b");
}

TEST(plus_and_encoded_plus_together) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("a+b%2Bc", output));
EXPECT_EQ(output, "a b+c");
}

TEST(mixed_plus_and_percent) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("a+b%20c%2Fd", output));
EXPECT_EQ(output, "a b c/d");
}

TEST(unreserved_characters_pass_through) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("ABCabc0123-._~", output));
EXPECT_EQ(output, "ABCabc0123-._~");
}

TEST(empty_input_succeeds) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("", output));
EXPECT_TRUE(output.empty());
}

TEST(appends_to_an_existing_output) {
std::string output{"key="};
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("a+b", output));
EXPECT_EQ(output, "key=a b");
}

TEST(rejects_a_trailing_percent) {
std::string output;
EXPECT_FALSE(sourcemeta::core::URI::unescape_form("abc%", output));
}

TEST(rejects_an_incomplete_sequence) {
std::string output;
EXPECT_FALSE(sourcemeta::core::URI::unescape_form("a%2", output));
}

TEST(rejects_a_non_hex_digit) {
std::string output;
EXPECT_FALSE(sourcemeta::core::URI::unescape_form("a%2Gb", output));
}

TEST(rejects_a_percent_before_the_end) {
std::string output;
EXPECT_FALSE(sourcemeta::core::URI::unescape_form("a%b", output));
}

TEST(restores_the_output_on_failure) {
// The prior content and every byte decoded before the malformed escape are
// rolled back, so a failed decode never leaves a partial value behind
std::string output{"prefix:"};
EXPECT_FALSE(sourcemeta::core::URI::unescape_form("good%20then%2", output));
EXPECT_EQ(output, "prefix:");
}

TEST(decodes_a_null_byte) {
std::string output;
EXPECT_TRUE(sourcemeta::core::URI::unescape_form("a%00b", output));
EXPECT_EQ(output.size(), 3);
EXPECT_EQ(output.front(), 'a');
EXPECT_EQ(static_cast<unsigned char>(output[1]), 0x00);
EXPECT_EQ(output.back(), 'b');
}
Loading