From ba43dbd1973a69e69a1a3587e908f5442f0e7277 Mon Sep 17 00:00:00 2001 From: vera-rubin <322284620+vera-rubin@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:59:03 +0000 Subject: [PATCH] fix: preserve reserved escapes during normalization --- include/boost/url/impl/url_base.hpp | 34 ++++++++++------------------- test/unit/compat/ada.cpp | 25 +++++++++------------ test/unit/url.cpp | 12 ++++++++++ 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/include/boost/url/impl/url_base.hpp b/include/boost/url/impl/url_base.hpp index 062330282..5c4220acd 100644 --- a/include/boost/url/impl/url_base.hpp +++ b/include/boost/url/impl/url_base.hpp @@ -2015,21 +2015,16 @@ normalize_path() // // Step 1: Percent-encoding normalization // - // Decode percent-encoded characters that are - // valid unencoded in path segments (pchar), + // Decode percent-encoded unreserved characters, // and uppercase remaining hex digits. // - // This can introduce: - // - New '.' from %2E (dot is unreserved) - // - New ':' from %3A (colon is a pchar) - // This cannot introduce: - // - New '/' (%2F stays encoded, '/' is not - // a segment character) + // This can introduce new '.' from %2E, + // but reserved characters stay encoded. // - // These new '.' and ':' characters can create + // These new '.' characters can create // ambiguities that Steps 2 and 3 must handle. // - normalize_octets_impl(id_path, detail::segment_chars, op); + normalize_octets_impl(id_path, unreserved_chars, op); core::string_view p = impl_.get(id_path); char* p_dest = s_ + impl_.offset(id_path); char* p_end = s_ + impl_.offset(id_path + 1); @@ -2256,15 +2251,11 @@ normalize_path() // misinterpreted as a scheme delimiter when // serialized and re-parsed. // - // Colons can appear in the first segment either - // because they were already there (decoded from - // %3A in Step 1), or because remove_dot_segments - // (Step 3) canceled preceding segments via ".." - // and exposed a colon that was deeper in the path. + // Colons can appear in the first segment because + // remove_dot_segments (Step 3) canceled preceding + // segments via ".." and exposed a colon that was + // deeper in the path. // - // Example (pre-existing): - // my%3Asharona -> Step 1: my:sharona - // -> Step 5: my%3Asharona // Example (exposed by dot removal): // a/../b:c -> Step 3: b:c // -> Step 5: b%3Ac @@ -2369,10 +2360,7 @@ normalize_query() { op_t op(*this); normalize_octets_impl( - id_query, - detail::query_chars, - detail::query_ignore_chars, - op); + id_query, unreserved_chars, op); return *this; } @@ -2383,7 +2371,7 @@ normalize_fragment() { op_t op(*this); normalize_octets_impl( - id_frag, detail::fragment_chars, op); + id_frag, unreserved_chars, op); return *this; } diff --git a/test/unit/compat/ada.cpp b/test/unit/compat/ada.cpp index 010b242ff..cb21bc4ff 100644 --- a/test/unit/compat/ada.cpp +++ b/test/unit/compat/ada.cpp @@ -2370,9 +2370,8 @@ struct ada_test // href: "http://example.com/(%28:%3A%29)" BOOST_TEST_CSTR_EQ(u.scheme(), "http"); BOOST_TEST_CSTR_EQ(u.encoded_host(), "example.com"); - // Boost.URL normalizes octets when resolving the URL - // BOOST_TEST_CSTR_EQ(u.encoded_path(), "/(%28:%3A%29)"); - BOOST_TEST_CSTR_EQ(u.encoded_path(), "/((::))"); + // Reserved escapes stay encoded. + BOOST_TEST_CSTR_EQ(u.encoded_path(), "/(%28:%3A%29)"); }(); []{ system::result base = parse_uri("about:blank"); @@ -2389,9 +2388,8 @@ struct ada_test // href: "http://example.com/%3A%3a%3C%3c" BOOST_TEST_CSTR_EQ(u.scheme(), "http"); BOOST_TEST_CSTR_EQ(u.encoded_host(), "example.com"); - // Boost.URL normalizes octets when resolving the URL - // BOOST_TEST_CSTR_EQ(u.encoded_path(), "/%3A%3A%3C%3C"); - BOOST_TEST_CSTR_EQ(u.encoded_path(), "/::%3C%3C"); + // Escape hex digits are normalized to uppercase. + BOOST_TEST_CSTR_EQ(u.encoded_path(), "/%3A%3A%3C%3C"); }(); []{ system::result base = parse_uri("about:blank"); @@ -2424,9 +2422,8 @@ struct ada_test // href: "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd" BOOST_TEST_CSTR_EQ(u.scheme(), "http"); BOOST_TEST_CSTR_EQ(u.encoded_host(), "example.com"); - // Boost.URL normalizes octets when resolving the URL - // BOOST_TEST_CSTR_EQ(u.encoded_path(), "/%7Ffp3%3Eju%3Dduvgw%3Dd"); - BOOST_TEST_CSTR_EQ(u.encoded_path(), "/%7Ffp3%3Eju=duvgw=d"); + // Reserved escapes stay encoded. + BOOST_TEST_CSTR_EQ(u.encoded_path(), "/%7Ffp3%3Eju%3Dduvgw%3Dd"); }(); []{ system::result base = parse_uri("about:blank"); @@ -2443,9 +2440,8 @@ struct ada_test // href: "http://example.com/@asdf%40" BOOST_TEST_CSTR_EQ(u.scheme(), "http"); BOOST_TEST_CSTR_EQ(u.encoded_host(), "example.com"); - // Boost.URL normalizes octets when resolving the URL - // BOOST_TEST_CSTR_EQ(u.encoded_path(), "/@asdf%40"); - BOOST_TEST_CSTR_EQ(u.encoded_path(), "/@asdf@"); + // Reserved escapes stay encoded. + BOOST_TEST_CSTR_EQ(u.encoded_path(), "/@asdf%40"); }(); []{ system::result base = parse_uri("about:blank"); @@ -6186,9 +6182,8 @@ struct ada_test u.normalize_path(); // href: "file:///C%3A/" BOOST_TEST_CSTR_EQ(u.scheme(), "file"); - // Boost.URL normalizes octets when resolving the URL - // BOOST_TEST_CSTR_EQ(u.encoded_path(), "/C%3A/"); - BOOST_TEST_CSTR_EQ(u.encoded_path(), "/C:/"); + // Reserved escapes stay encoded. + BOOST_TEST_CSTR_EQ(u.encoded_path(), "/C%3A/"); }(); []{ system::result base = parse_uri("about:blank"); diff --git a/test/unit/url.cpp b/test/unit/url.cpp index b078729f6..34681cbf4 100644 --- a/test/unit/url.cpp +++ b/test/unit/url.cpp @@ -1279,6 +1279,18 @@ struct url_test check("HtTp://cppalliance.org/%2F", "http://cppalliance.org/%2F"); + // Reserved escapes stay encoded during normalization. + check("https://example.com/item%3bname", + "https://example.com/item%3Bname"); + check("https://example.com/?q=a%2fb", + "https://example.com/?q=a%2Fb"); + check("https://example.com/#a%2fb", + "https://example.com/#a%2Fb"); + check("https://example.com/#a%23b", + "https://example.com/#a%23b"); + check("http://example.com/%7e%3b%41?%71=%2f%7e#%61%23%2f", + "http://example.com/~%3BA?q=%2F~#a%23%2F"); + } // issue 985