From b1f1418b2ed0e682731281433cd6be9066894daf Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Thu, 28 Aug 2025 19:10:57 -0500 Subject: [PATCH] feat: zone_id setters --- include/boost/url/static_url.hpp | 4 ++ include/boost/url/url.hpp | 4 ++ include/boost/url/url_base.hpp | 67 ++++++++++++++++++++ include/boost/url/url_view_base.hpp | 20 +++--- src/rfc/detail/ip_literal_rule.cpp | 2 +- src/url_base.cpp | 95 +++++++++++++++++++++++++---- test/unit/url_base.cpp | 35 +++++++++++ 7 files changed, 205 insertions(+), 22 deletions(-) diff --git a/include/boost/url/static_url.hpp b/include/boost/url/static_url.hpp index 0343dbfc..5313135d 100644 --- a/include/boost/url/static_url.hpp +++ b/include/boost/url/static_url.hpp @@ -384,6 +384,10 @@ public: static_url& set_host_ipv4(ipv4_address const& addr) { url_base::set_host_ipv4(addr); return *this; } /// @copydoc url_base::set_host_ipv6 static_url& set_host_ipv6(ipv6_address const& addr) { url_base::set_host_ipv6(addr); return *this; } + /// @copydoc url_base::set_zone_id + static_url& set_zone_id(core::string_view s) { url_base::set_zone_id(s); return *this; } + /// @copydoc url_base::set_encoded_zone_id + static_url& set_encoded_zone_id(pct_string_view const& s) { url_base::set_encoded_zone_id(s); return *this; } /// @copydoc url_base::set_host_ipvfuture static_url& set_host_ipvfuture(core::string_view s) { url_base::set_host_ipvfuture(s); return *this; } /// @copydoc url_base::set_host_name diff --git a/include/boost/url/url.hpp b/include/boost/url/url.hpp index 2bf4684b..88126f92 100644 --- a/include/boost/url/url.hpp +++ b/include/boost/url/url.hpp @@ -450,6 +450,10 @@ public: url& set_host_ipv4(ipv4_address const& addr) { url_base::set_host_ipv4(addr); return *this; } /// @copydoc url_base::set_host_ipv6 url& set_host_ipv6(ipv6_address const& addr) { url_base::set_host_ipv6(addr); return *this; } + /// @copydoc url_base::set_zone_id + url& set_zone_id(core::string_view s) { url_base::set_zone_id(s); return *this; } + /// @copydoc url_base::set_encoded_zone_id + url& set_encoded_zone_id(pct_string_view const& s) { url_base::set_encoded_zone_id(s); return *this; } /// @copydoc url_base::set_host_ipvfuture url& set_host_ipvfuture(core::string_view s) { url_base::set_host_ipvfuture(s); return *this; } /// @copydoc url_base::set_host_name diff --git a/include/boost/url/url_base.hpp b/include/boost/url/url_base.hpp index f630c309..273a362b 100644 --- a/include/boost/url/url_base.hpp +++ b/include/boost/url/url_base.hpp @@ -1355,6 +1355,63 @@ public: set_host_ipv6( ipv6_address const& addr); + /** Set the zone ID for an IPv6 address. + + This function sets the zone ID for the host if the host is an IPv6 address. + Reserved characters in the string are percent-escaped in the result. + + @par Example + @code + assert( u.set_host_ipv6( ipv6_address( "fe80::1" ) ).set_zone_id( "eth0" ).buffer() == "https://[fe80::1%25eth0]" ); + @endcode + + @par Complexity + Linear in `this->size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @param s The zone ID to set. + @return `*this` + + @par Specification + @li RFC 6874 + + */ + url_base& + set_zone_id(core::string_view s); + + /** Set the zone ID for an IPv6 address (percent-encoded). + + This function sets the zone ID for the host if the host is an IPv6 address. + Escapes in the string are preserved, and reserved characters in the string + are percent-escaped in the result. + + @par Example + @code + assert( u.set_host_ipv6( ipv6_address( "fe80::1" ) ).set_encoded_zone_id( "eth0" ).buffer() == "https://[fe80::1%25eth0]" ); + @endcode + + @par Complexity + Linear in `this->size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + Exceptions thrown on invalid input. + + @throw system_error + `s` contains an invalid percent-encoding. + + @param s The zone ID to set. + @return `*this` + + @par Specification + @li RFC 6874 + + */ + url_base& + set_encoded_zone_id(pct_string_view s); + /** Set the host to an address The host is set to the specified IPvFuture @@ -2861,6 +2918,16 @@ private: char* set_port_impl(std::size_t n, op_t& op); char* set_path_impl(std::size_t n, op_t& op); + void + set_host_ipv6_and_zone_id( + ipv6_address const& addr, + core::string_view zone_id); + + void + set_host_ipv6_and_encoded_zone_id( + ipv6_address const& addr, + pct_string_view zone_id); + core::string_view first_segment() const noexcept; diff --git a/include/boost/url/url_view_base.hpp b/include/boost/url/url_view_base.hpp index 3ca796c8..84477fe6 100644 --- a/include/boost/url/url_view_base.hpp +++ b/include/boost/url/url_view_base.hpp @@ -736,7 +736,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_userinfo().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the userinfo @@ -836,7 +836,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_user().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the user @@ -935,7 +935,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_password().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the password @@ -1064,7 +1064,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_host().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the host @@ -1162,7 +1162,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_host_address().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the host @@ -1383,7 +1383,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_host_name().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the host name @@ -1468,7 +1468,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_zone_id().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the IPv6 Zone ID @@ -1732,7 +1732,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_path().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the path @@ -1977,7 +1977,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_query().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the query @@ -2220,7 +2220,7 @@ public: encoding_opts opt; opt.space_as_plus = false; return encoded_fragment().decode( - opt, std::move(token)); + opt, std::forward(token)); } /** Return the fragment diff --git a/src/rfc/detail/ip_literal_rule.cpp b/src/rfc/detail/ip_literal_rule.cpp index 88bbdb7a..6eebada4 100644 --- a/src/rfc/detail/ip_literal_rule.cpp +++ b/src/rfc/detail/ip_literal_rule.cpp @@ -57,7 +57,7 @@ parse( grammar::delim_rule(']')))); if(! rv) { - // IPv6addrz + // IPv6addrz: https://datatracker.ietf.org/doc/html/rfc6874 it = it0; auto rv2 = grammar::parse( it, end, diff --git a/src/url_base.cpp b/src/url_base.cpp index 2617351e..6772c4bd 100644 --- a/src/url_base.cpp +++ b/src/url_base.cpp @@ -810,24 +810,97 @@ url_base:: set_host_ipv6( ipv6_address const& addr) { - op_t op(*this); - char buf[2 + - urls::ipv6_address::max_str_len]; - auto s = addr.to_buffer( - buf + 1, sizeof(buf) - 2); - buf[0] = '['; - buf[s.size() + 1] = ']'; - auto const n = s.size() + 2; + set_host_ipv6_and_encoded_zone_id(addr, encoded_zone_id()); + return *this; +} + +url_base& +url_base:: +set_zone_id(core::string_view s) +{ + set_host_ipv6_and_zone_id(host_ipv6_address(), s); + return *this; +} + +url_base& +url_base:: +set_encoded_zone_id(pct_string_view s) +{ + set_host_ipv6_and_encoded_zone_id(host_ipv6_address(), s); + return *this; +} + +void +url_base:: +set_host_ipv6_and_zone_id( + ipv6_address const& addr, + core::string_view zone_id) +{ + op_t op(*this, &zone_id); + char ipv6_str_buf[urls::ipv6_address::max_str_len]; + auto ipv6_str = addr.to_buffer(ipv6_str_buf, sizeof(ipv6_str_buf)); + bool const has_zone_id = !zone_id.empty(); + encoding_opts opt; + auto const ipn = ipv6_str.size(); + auto const zn = encoded_size(zone_id, unreserved_chars, opt); + auto const n = ipn + 2 + has_zone_id * (3 + zn); auto dest = set_host_impl(n, op); - std::memcpy(dest, buf, n); - impl_.decoded_[id_host] = n; + *dest++ = '['; + std::memcpy(dest, ipv6_str.data(), ipn); + dest += ipn; + if (has_zone_id) + { + *dest++ = '%'; + *dest++ = '2'; + *dest++ = '5'; + encode(dest, zn, zone_id, unreserved_chars, opt); + dest += zn; + } + *dest++ = ']'; + // ipn + |"["| + |"]"| + (has_zone_id ? |"%"| + zn : 0) + impl_.decoded_[id_host] = ipn + 2 + has_zone_id * (1 + zone_id.size()); + impl_.host_type_ = urls::host_type::ipv6; + auto bytes = addr.to_bytes(); + std::memcpy( + impl_.ip_addr_, + bytes.data(), + bytes.size()); +} + +void +url_base:: +set_host_ipv6_and_encoded_zone_id( + ipv6_address const& addr, + pct_string_view zone_id) +{ + op_t op(*this, &detail::ref(zone_id)); + char ipv6_str_buf[urls::ipv6_address::max_str_len]; + auto ipv6_str = addr.to_buffer(ipv6_str_buf, sizeof(ipv6_str_buf)); + bool const has_zone_id = !zone_id.empty(); + auto const ipn = ipv6_str.size(); + auto const zn = detail::re_encoded_size_unsafe(zone_id, unreserved_chars); + auto const n = ipn + 2 + has_zone_id * (3 + zn); + auto dest = set_host_impl(n, op); + *dest++ = '['; + std::memcpy(dest, ipv6_str.data(), ipn); + dest += ipn; + std::size_t dzn = 0; + if (has_zone_id) + { + *dest++ = '%'; + *dest++ = '2'; + *dest++ = '5'; + dzn = detail::re_encode_unsafe(dest, dest + zn, zone_id, unreserved_chars); + } + *dest++ = ']'; + // ipn + |"["| + |"]"| + (has_zone_id ? |"%"| + zn : 0) + impl_.decoded_[id_host] = ipn + 2 + has_zone_id * (1 + dzn); impl_.host_type_ = urls::host_type::ipv6; auto bytes = addr.to_bytes(); std::memcpy( impl_.ip_addr_, bytes.data(), bytes.size()); - return *this; } url_base& diff --git a/test/unit/url_base.cpp b/test/unit/url_base.cpp index 5d33aee7..b44624fb 100644 --- a/test/unit/url_base.cpp +++ b/test/unit/url_base.cpp @@ -1192,6 +1192,41 @@ struct url_base_test set_host_ipv6("1::6:c0a8:1", "//[1::6:c0a8:1]"); + // set_zone_id + { + // Round-trip: set and get zone id + url u; + BOOST_TEST_NO_THROW(u.set_host_ipv6(ipv6_address("fe80::1"))); + BOOST_TEST_NO_THROW(u.set_zone_id("eth0")); + BOOST_TEST_EQ(u.host_ipv6_address().to_string(), "fe80::1"); + BOOST_TEST_EQ(u.zone_id(), "eth0"); + BOOST_TEST_EQ(u.buffer(), "//[fe80::1%25eth0]"); + + // set_zone_id when no IPv6 host: should create default + // constructed IPv6 + url u2; + BOOST_TEST_NO_THROW(u2.set_zone_id("zone42")); + BOOST_TEST_EQ(u2.host_type(), host_type::ipv6); + BOOST_TEST_EQ(u2.zone_id(), "zone42"); + BOOST_TEST_EQ(u2.buffer(), "//[::%25zone42]"); + + // set_encoded_zone_id: round-trip + url u3; + BOOST_TEST_NO_THROW(u3.set_host_ipv6(ipv6_address("fe80::2"))); + BOOST_TEST_NO_THROW(u3.set_encoded_zone_id("en%30")); + BOOST_TEST_EQ(u3.zone_id(), "en0"); + BOOST_TEST_EQ(u3.encoded_zone_id(), "en%30"); + BOOST_TEST_EQ(u3.buffer(), "//[fe80::2%25en%30]"); + + // set_encoded_zone_id when no IPv6 host: should create default + // constructed IPv6 + url u4; + BOOST_TEST_NO_THROW(u4.set_encoded_zone_id("zone%34")); + BOOST_TEST_EQ(u4.host_type(), host_type::ipv6); + BOOST_TEST_EQ(u4.zone_id(), "zone4"); + BOOST_TEST_EQ(u4.buffer(), "//[::%25zone%34]"); + } + set_host_ipvfuture("v42.69", "//[v42.69]"); BOOST_TEST_THROWS(url().set_host_ipvfuture("127.0.0.1"), system::system_error);