From 2004668509e983a1b5a8bed6f4b9c87840330df4 Mon Sep 17 00:00:00 2001 From: Denis Gregor Date: Mon, 17 Aug 2026 19:27:57 -0500 Subject: [PATCH] Make decode_uri the inverse of encode_uri (#2540) decode_uri was a byte-for-byte copy of decode_uri_component: it decoded every %XX, including escapes of the reserved characters that encode_uri leaves literal. So decode_uri was not the inverse of encode_uri and promoted an escaped delimiter into a real one -- decode_uri("http://h/a%2Fb") returned "http://h/a/b". Keep escapes of the reserved set encode_uri preserves, matching JS decodeURI; non-reserved escapes still decode. --- httplib.h | 14 +++++++++++++- test/test.cc | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index 94c50e8f..31db7884 100644 --- a/httplib.h +++ b/httplib.h @@ -10603,7 +10603,19 @@ inline std::string decode_uri(const std::string &value) { if (value[i] == '%' && i + 2 < value.size()) { auto val = 0; if (detail::from_hex_to_i(value, i + 1, 2, val)) { - result += static_cast(val); + auto c = static_cast(val); + // Keep escapes of the reserved characters that encode_uri leaves + // literal, so decode_uri is the inverse of encode_uri and an escaped + // delimiter is not promoted into a real one (as with JS decodeURI). + if (c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || + c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || + c == '#') { + result += value[i]; + result += value[i + 1]; + result += value[i + 2]; + } else { + result += c; + } i += 2; } else { result += value[i]; diff --git a/test/test.cc b/test/test.cc index ff8bfb03..2d18aefd 100644 --- a/test/test.cc +++ b/test/test.cc @@ -719,6 +719,22 @@ TEST(DecodeUriTest, TestRoundTripWithEncodeUri) { EXPECT_EQ(decoded, original); } +TEST(DecodeUriTest, KeepsReservedCharacterEscapes) { + // decode_uri is the inverse of encode_uri: an escaped reserved character + // stays encoded so it is not promoted into a real delimiter, while + // non-reserved escapes still decode (like JS decodeURI). + EXPECT_EQ(httplib::decode_uri("%2F"), "%2F"); + EXPECT_EQ(httplib::decode_uri("%23"), "%23"); + EXPECT_EQ(httplib::decode_uri("%3F%3A%40%26%3D%2B%24%2C%3B"), + "%3F%3A%40%26%3D%2B%24%2C%3B"); + EXPECT_EQ(httplib::decode_uri("%2D"), "-"); + EXPECT_EQ(httplib::decode_uri("%20"), " "); + EXPECT_EQ(httplib::decode_uri("http://example.com/a%2Fb"), + "http://example.com/a%2Fb"); + // decode_uri_component still decodes the reserved character. + EXPECT_EQ(httplib::decode_uri_component("%2F"), "/"); +} + TEST(DecodeUriComponentTest, TestRoundTripWithEncodeUriComponent) { string original = "Piri Tommy Villiers - on & on"; string encoded = httplib::encode_uri_component(original);