#include "string_utils.hpp"
+#include <FormatNonstdStringView.hpp>
+#include <fmtmacros.hpp>
+
+// System headers
+#include <cctype>
+// End of system headers
+
namespace util {
+nonstd::expected<std::string, std::string>
+percent_decode(nonstd::string_view string)
+{
+ const auto from_hex = [](const char digit) {
+ return static_cast<uint8_t>(
+ std::isdigit(digit) ? digit - '0' : std::tolower(digit) - 'a' + 10);
+ };
+
+ std::string result;
+ result.reserve(string.size());
+ for (size_t i = 0; i < string.size(); ++i) {
+ if (string[i] != '%') {
+ result += string[i];
+ } else if (i + 2 >= string.size() || !std::isxdigit(string[i + 1])
+ || !std::isxdigit(string[i + 2])) {
+ return nonstd::make_unexpected(
+ FMT("invalid percent-encoded string at position {}: {}", i, string));
+ } else {
+ const char ch = static_cast<char>(from_hex(string[i + 1]) << 4
+ | from_hex(string[i + 2]));
+ result += ch;
+ i += 2;
+ }
+ }
+
+ return result;
+}
+
std::pair<nonstd::string_view, nonstd::optional<nonstd::string_view>>
split_once(const nonstd::string_view string, const char split_char)
{
#pragma once
+#include <third_party/nonstd/expected.hpp>
#include <third_party/nonstd/optional.hpp>
#include <third_party/nonstd/string_view.hpp>
namespace util {
+// Percent-decode[1] `string`.
+//
+// [1]: https://en.wikipedia.org/wiki/Percent-encoding
+nonstd::expected<std::string, std::string>
+percent_decode(nonstd::string_view string);
+
// Split `string` into two parts using `split_char` as the delimiter. The second
// part will be `nullopt` if there is no `split_char` in `string.`
std::pair<nonstd::string_view, nonstd::optional<nonstd::string_view>>
return left.first == right.first && left.second == right.second;
}
+TEST_CASE("util::percent_decode")
+{
+ CHECK(util::percent_decode("") == "");
+ CHECK(util::percent_decode("a") == "a");
+ CHECK(util::percent_decode("%61") == "a");
+ CHECK(util::percent_decode("%ab") == "\xab");
+ CHECK(util::percent_decode("%aB") == "\xab");
+ CHECK(util::percent_decode("%Ab") == "\xab");
+ CHECK(util::percent_decode("%AB") == "\xab");
+ CHECK(util::percent_decode("a%25b%7cc") == "a%b|c");
+
+ CHECK(util::percent_decode("%").error()
+ == "invalid percent-encoded string at position 0: %");
+ CHECK(util::percent_decode("%6").error()
+ == "invalid percent-encoded string at position 0: %6");
+ CHECK(util::percent_decode("%%").error()
+ == "invalid percent-encoded string at position 0: %%");
+ CHECK(util::percent_decode("a%0g").error()
+ == "invalid percent-encoded string at position 1: a%0g");
+}
+
TEST_CASE("util::split_once")
{
using nonstd::nullopt;