From 83effe352f810ac79edad189771a466b05b73156 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 24 Nov 2025 16:58:51 -0500 Subject: [PATCH 1/4] add URLSearchParams::to_raw_string() method --- include/ada/url_search_params-inl.h | 17 +++++++++++ include/ada/url_search_params.h | 7 +++++ include/ada_c.h | 1 + src/ada_c.cpp | 12 ++++++++ tests/ada_c.cpp | 43 +++++++++++++++++++++++++++ tests/url_search_params.cpp | 46 +++++++++++++++++++++++++++++ 6 files changed, 126 insertions(+) diff --git a/include/ada/url_search_params-inl.h b/include/ada/url_search_params-inl.h index 5787a0598..67f043a46 100644 --- a/include/ada/url_search_params-inl.h +++ b/include/ada/url_search_params-inl.h @@ -134,6 +134,23 @@ inline std::string url_search_params::to_string() const { return out; } +inline std::string url_search_params::to_raw_string() const { + auto character_set = ada::character_sets::QUERY_PERCENT_ENCODE; + std::string out{}; + for (size_t i = 0; i < params.size(); i++) { + auto key = ada::unicode::percent_encode(params[i].first, character_set); + auto value = ada::unicode::percent_encode(params[i].second, character_set); + + if (i != 0) { + out += "&"; + } + out.append(key); + out += "="; + out.append(value); + } + return out; +} + inline void url_search_params::set(const std::string_view key, const std::string_view value) { const auto find = [&key](const auto ¶m) { return param.first == key; }; diff --git a/include/ada/url_search_params.h b/include/ada/url_search_params.h index fb8ead258..57318c694 100644 --- a/include/ada/url_search_params.h +++ b/include/ada/url_search_params.h @@ -100,6 +100,13 @@ struct url_search_params { */ inline std::string to_string() const; + /** + * Returns a serialized query string without normalizing the key-value pairs. + * Unlike to_string(), this method does not apply additional transformations + * to the percent-encoded output. + */ + inline std::string to_raw_string() const; + /** * Returns a simple JS-style iterator over all of the keys in this * url_search_params. The keys in the iterator are not unique. The valid diff --git a/include/ada_c.h b/include/ada_c.h index 5e124984c..6ac31387e 100644 --- a/include/ada_c.h +++ b/include/ada_c.h @@ -130,6 +130,7 @@ void ada_free_search_params(ada_url_search_params result); size_t ada_search_params_size(ada_url_search_params result); void ada_search_params_sort(ada_url_search_params result); ada_owned_string ada_search_params_to_string(ada_url_search_params result); +ada_owned_string ada_search_params_to_raw_string(ada_url_search_params result); void ada_search_params_append(ada_url_search_params result, const char* key, size_t key_length, const char* value, diff --git a/src/ada_c.cpp b/src/ada_c.cpp index 48a344604..476ca9c02 100644 --- a/src/ada_c.cpp +++ b/src/ada_c.cpp @@ -487,6 +487,18 @@ ada_owned_string ada_search_params_to_string(ada_url_search_params result) { return owned; } +ada_owned_string ada_search_params_to_raw_string(ada_url_search_params result) { + ada::result& r = + *(ada::result*)result; + if (!r) return ada_owned_string{nullptr, 0}; + std::string out = r->to_raw_string(); + ada_owned_string owned{}; + owned.length = out.size(); + owned.data = new char[owned.length]; + memcpy((void*)owned.data, out.data(), owned.length); + return owned; +} + size_t ada_search_params_size(ada_url_search_params result) { ada::result& r = *(ada::result*)result; diff --git a/tests/ada_c.cpp b/tests/ada_c.cpp index 692e704de..e32dffc5a 100644 --- a/tests/ada_c.cpp +++ b/tests/ada_c.cpp @@ -357,6 +357,49 @@ TEST(ada_c, ada_url_search_params) { SUCCEED(); } +TEST(ada_c, ada_search_params_to_raw_string) { + std::string input("a=b c&d=e+f"); + auto out = ada_parse_search_params(input.c_str(), input.length()); + + // Note: + in input is decoded to space during parsing + // to_string normalizes spaces to + + ada_owned_string str = ada_search_params_to_string(out); + ASSERT_EQ(convert_string(str), "a=b+c&d=e+f"); + ada_free_owned_string(str); + + // to_raw_string preserves %20 encoding for spaces + ada_owned_string raw_str = ada_search_params_to_raw_string(out); + ASSERT_EQ(convert_string(raw_str), "a=b%20c&d=e%20f"); + ada_free_owned_string(raw_str); + + ada_free_search_params(out); + + SUCCEED(); +} + +TEST(ada_c, ada_search_params_to_raw_string_remove_preserves_encoding) { + // Test the exact scenario from the issue + std::string input("a=%20&b=remove&c=2"); + auto params = ada_parse_search_params(input.c_str(), input.length()); + + // Remove parameter "b" + ada_search_params_remove(params, "b", 1); + + // to_string normalizes space to + + ada_owned_string str = ada_search_params_to_string(params); + ASSERT_EQ(convert_string(str), "a=+&c=2"); + ada_free_owned_string(str); + + // to_raw_string preserves %20 encoding for spaces + ada_owned_string raw_str = ada_search_params_to_raw_string(params); + ASSERT_EQ(convert_string(raw_str), "a=%20&c=2"); + ada_free_owned_string(raw_str); + + ada_free_search_params(params); + + SUCCEED(); +} + TEST(ada_c, ada_get_version) { std::string_view raw = ada_get_version(); ada_version_components parsed = ada_get_version_components(); diff --git a/tests/url_search_params.cpp b/tests/url_search_params.cpp index da3017e7d..3cec626c0 100644 --- a/tests/url_search_params.cpp +++ b/tests/url_search_params.cpp @@ -448,3 +448,49 @@ TEST(url_search_params, sort_unicode_code_units_edge_case) { ASSERT_EQ(keys.next(), "\xf0\x9f\x8c\x88\xef\xac\x83"); SUCCEED(); } + +TEST(url_search_params, to_raw_string_no_normalization) { + auto params = ada::url_search_params(); + params.append("a", "b c"); + // to_string normalizes space to + + ASSERT_EQ(params.to_string(), "a=b+c"); + // to_raw_string preserves %20 encoding + ASSERT_EQ(params.to_raw_string(), "a=b%20c"); + SUCCEED(); +} + +TEST(url_search_params, to_raw_string_with_special_chars) { + auto params = ada::url_search_params(); + params.append("key1", "value with spaces"); + params.append("key2", "another value"); + // to_string normalizes spaces to + + ASSERT_EQ(params.to_string(), "key1=value+with+spaces&key2=another+value"); + // to_raw_string preserves %20 encoding + ASSERT_EQ(params.to_raw_string(), + "key1=value%20with%20spaces&key2=another%20value"); + SUCCEED(); +} + +TEST(url_search_params, to_raw_string_with_accents) { + auto params = ada::url_search_params(); + params.append("key1", "\u00E9t\u00E9"); + params.append("key2", "C\u00E9line Dion++"); + // Both should encode accents the same way + // to_string normalizes spaces to +, to_raw_string uses %20 + // Note: + signs are not encoded by QUERY_PERCENT_ENCODE + ASSERT_EQ(params.to_string(), + "key1=%C3%A9t%C3%A9&key2=C%C3%A9line+Dion%2B%2B"); + ASSERT_EQ(params.to_raw_string(), + "key1=%C3%A9t%C3%A9&key2=C%C3%A9line%20Dion++"); + SUCCEED(); +} + +TEST(url_search_params, to_raw_string_empty_values) { + auto params = ada::url_search_params(); + params.append("a", ""); + params.append("", "b"); + params.append("", ""); + ASSERT_EQ(params.to_raw_string(), "a=&=b&="); + ASSERT_EQ(params.to_string(), "a=&=b&="); + SUCCEED(); +} From a0d2bc6c9c971deb68ad4861876a581ca747ec03 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 1 Dec 2025 12:26:43 -0500 Subject: [PATCH 2/4] address pr reviews --- include/ada/url_search_params-inl.h | 8 ++------ tests/ada_c.cpp | 11 +++++------ tests/url_search_params.cpp | 15 +++++++-------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/include/ada/url_search_params-inl.h b/include/ada/url_search_params-inl.h index 67f043a46..256418dde 100644 --- a/include/ada/url_search_params-inl.h +++ b/include/ada/url_search_params-inl.h @@ -135,13 +135,9 @@ inline std::string url_search_params::to_string() const { } inline std::string url_search_params::to_raw_string() const { - auto character_set = ada::character_sets::QUERY_PERCENT_ENCODE; std::string out{}; - for (size_t i = 0; i < params.size(); i++) { - auto key = ada::unicode::percent_encode(params[i].first, character_set); - auto value = ada::unicode::percent_encode(params[i].second, character_set); - - if (i != 0) { + for (const auto &[key, value] : params) { + if (!out.empty()) { out += "&"; } out.append(key); diff --git a/tests/ada_c.cpp b/tests/ada_c.cpp index e32dffc5a..b5461bc10 100644 --- a/tests/ada_c.cpp +++ b/tests/ada_c.cpp @@ -367,9 +367,9 @@ TEST(ada_c, ada_search_params_to_raw_string) { ASSERT_EQ(convert_string(str), "a=b+c&d=e+f"); ada_free_owned_string(str); - // to_raw_string preserves %20 encoding for spaces + // to_raw_string outputs raw key/value without any encoding ada_owned_string raw_str = ada_search_params_to_raw_string(out); - ASSERT_EQ(convert_string(raw_str), "a=b%20c&d=e%20f"); + ASSERT_EQ(convert_string(raw_str), "a=b c&d=e f"); ada_free_owned_string(raw_str); ada_free_search_params(out); @@ -377,8 +377,7 @@ TEST(ada_c, ada_search_params_to_raw_string) { SUCCEED(); } -TEST(ada_c, ada_search_params_to_raw_string_remove_preserves_encoding) { - // Test the exact scenario from the issue +TEST(ada_c, ada_search_params_to_raw_string_remove) { std::string input("a=%20&b=remove&c=2"); auto params = ada_parse_search_params(input.c_str(), input.length()); @@ -390,9 +389,9 @@ TEST(ada_c, ada_search_params_to_raw_string_remove_preserves_encoding) { ASSERT_EQ(convert_string(str), "a=+&c=2"); ada_free_owned_string(str); - // to_raw_string preserves %20 encoding for spaces + // to_raw_string outputs raw key/value without any encoding ada_owned_string raw_str = ada_search_params_to_raw_string(params); - ASSERT_EQ(convert_string(raw_str), "a=%20&c=2"); + ASSERT_EQ(convert_string(raw_str), "a= &c=2"); ada_free_owned_string(raw_str); ada_free_search_params(params); diff --git a/tests/url_search_params.cpp b/tests/url_search_params.cpp index 3cec626c0..090257d35 100644 --- a/tests/url_search_params.cpp +++ b/tests/url_search_params.cpp @@ -454,8 +454,8 @@ TEST(url_search_params, to_raw_string_no_normalization) { params.append("a", "b c"); // to_string normalizes space to + ASSERT_EQ(params.to_string(), "a=b+c"); - // to_raw_string preserves %20 encoding - ASSERT_EQ(params.to_raw_string(), "a=b%20c"); + // to_raw_string outputs raw key/value without any encoding + ASSERT_EQ(params.to_raw_string(), "a=b c"); SUCCEED(); } @@ -465,9 +465,9 @@ TEST(url_search_params, to_raw_string_with_special_chars) { params.append("key2", "another value"); // to_string normalizes spaces to + ASSERT_EQ(params.to_string(), "key1=value+with+spaces&key2=another+value"); - // to_raw_string preserves %20 encoding + // to_raw_string outputs raw key/value without any encoding ASSERT_EQ(params.to_raw_string(), - "key1=value%20with%20spaces&key2=another%20value"); + "key1=value with spaces&key2=another value"); SUCCEED(); } @@ -475,13 +475,12 @@ TEST(url_search_params, to_raw_string_with_accents) { auto params = ada::url_search_params(); params.append("key1", "\u00E9t\u00E9"); params.append("key2", "C\u00E9line Dion++"); - // Both should encode accents the same way - // to_string normalizes spaces to +, to_raw_string uses %20 - // Note: + signs are not encoded by QUERY_PERCENT_ENCODE + // to_string percent-encodes and normalizes spaces to + ASSERT_EQ(params.to_string(), "key1=%C3%A9t%C3%A9&key2=C%C3%A9line+Dion%2B%2B"); + // to_raw_string outputs raw key/value without any encoding ASSERT_EQ(params.to_raw_string(), - "key1=%C3%A9t%C3%A9&key2=C%C3%A9line%20Dion++"); + "key1=\u00E9t\u00E9&key2=C\u00E9line Dion++"); SUCCEED(); } From 640b73b556f21dcc55058aa5925c49f17fc0e4b7 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 1 Dec 2025 14:18:00 -0500 Subject: [PATCH 3/4] pushing a new test --- tests/url_search_params.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/url_search_params.cpp b/tests/url_search_params.cpp index 090257d35..793d3a596 100644 --- a/tests/url_search_params.cpp +++ b/tests/url_search_params.cpp @@ -493,3 +493,14 @@ TEST(url_search_params, to_raw_string_empty_values) { ASSERT_EQ(params.to_string(), "a=&=b&="); SUCCEED(); } + + +TEST(url_search_params, with_ampersands) { + auto params = ada::url_search_params(); + params.append("a", "&"); + params.append("b", "?"); + params.append("b", "+"); + ASSERT_EQ(params.to_string(), "a=%26&b=%3F&b=%2B"); + ASSERT_EQ(params.to_raw_string(), "a=&&b=?&b=+"); + SUCCEED(); +} From e7a1c4f4ee1cd462fc396311dd08afc407463ec1 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 1 Dec 2025 14:27:22 -0500 Subject: [PATCH 4/4] lint --- tests/url_search_params.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/url_search_params.cpp b/tests/url_search_params.cpp index 793d3a596..09fd75328 100644 --- a/tests/url_search_params.cpp +++ b/tests/url_search_params.cpp @@ -494,7 +494,6 @@ TEST(url_search_params, to_raw_string_empty_values) { SUCCEED(); } - TEST(url_search_params, with_ampersands) { auto params = ada::url_search_params(); params.append("a", "&");