diff --git a/httplib.h b/httplib.h index df7ce281..37491fdf 100644 --- a/httplib.h +++ b/httplib.h @@ -11612,8 +11612,14 @@ inline std::string decode_path_component(const std::string &component) { // so to_utf8 writes at most 3 bytes. buff[4] is safe. char buff[4]; size_t len = detail::to_utf8(val, buff); - if (len > 0) { result.append(buff, len); } - i += 5; // 'u0000' + if (len > 0) { + result.append(buff, len); + i += 5; // 'u0000' + } else { + // A surrogate is no character. Dropping it would join what is on + // either side of it, so the escape is kept as it is. + result += component[i]; + } } else { result += component[i]; } diff --git a/test/test.cc b/test/test.cc index f23d2766..9c9b9e44 100644 --- a/test/test.cc +++ b/test/test.cc @@ -459,8 +459,10 @@ TEST(DecodePathTest, UnicodeEncoding) { EXPECT_EQ(U8("あ"), decode_path_component("%u3042")); // %uFFFF = max 4-digit hex (3-byte UTF-8, must not overflow buff[4]) EXPECT_FALSE(decode_path_component("%uFFFF").empty()); - // %uD800 = surrogate (invalid, silently dropped) - EXPECT_EQ("", decode_path_component("%uD800")); + // %uD800 = surrogate (invalid, kept as it is rather than joining its + // neighbours) + EXPECT_EQ("%uD800", decode_path_component("%uD800")); + EXPECT_EQ("/ad%uD800min", decode_path_component("/ad%uD800min")); } TEST(DecodeQueryTest, RejectsNonHexEscapes) {