From 9cc366525a1f4dfb749e655a966b3b3154216279 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Wed, 25 Feb 2026 18:15:18 -0500 Subject: [PATCH 01/21] build: use glob-tree-ex for test source discovery in Jamfile Replace the manually maintained SOURCES list with glob-tree-ex to auto-discover test .cpp files, matching the CMakeLists.txt approach. Files needing special flags (doc_grammar.cpp, doc_3_urls.cpp, example/) are excluded from the glob and handled by explicit run statements. --- test/unit/Jamfile | 85 +---------------------------------------------- 1 file changed, 1 insertion(+), 84 deletions(-) diff --git a/test/unit/Jamfile b/test/unit/Jamfile index 09424e4ec..0e8a1f3db 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -20,90 +20,7 @@ project ../../example/router ; -local SOURCES = - authority_view.cpp - error.cpp - error_types.cpp - decode.cpp - encode.cpp - encoding_opts.cpp - decode_view.cpp - format.cpp - grammar.cpp - host_type.cpp - ignore_case.cpp - ipv4_address.cpp - ipv6_address.cpp - optional.cpp - param.cpp - params_base.cpp - params_encoded_view.cpp - params_view.cpp - params_encoded_base.cpp - params_encoded_ref.cpp - params_ref.cpp - parse.cpp - parse_path.cpp - parse_query.cpp - pct_string_view.cpp - scheme.cpp - segments_base.cpp - segments_encoded_base.cpp - segments_encoded_ref.cpp - segments_encoded_view.cpp - segments_ref.cpp - segments_view.cpp - snippets.cpp - static_url.cpp - string_view.cpp - url.cpp - url_base.cpp - url_view.cpp - url_view_base.cpp - urls.cpp - variant.cpp - grammar/alnum_chars.cpp - grammar/alpha_chars.cpp - grammar/charset.cpp - grammar/ci_string.cpp - grammar/dec_octet_rule.cpp - grammar/delim_rule.cpp - grammar/digit_chars.cpp - grammar/grammar_error.cpp - grammar/grammar_parse.cpp - grammar/hexdig_chars.cpp - grammar/literal_rule.cpp - grammar/lut_chars.cpp - grammar/not_empty_rule.cpp - grammar/optional_rule.cpp - grammar/range_rule.cpp - grammar/recycled.cpp - grammar/string_token.cpp - grammar/string_view_base.cpp - grammar/token_rule.cpp - grammar/tuple_rule.cpp - grammar/type_traits.cpp - grammar/unsigned_rule.cpp - grammar/variant_rule.cpp - grammar/vchars.cpp - rfc/absolute_uri_rule.cpp - rfc/authority_rule.cpp - rfc/gen_delim_chars.cpp - rfc/ipv4_address_rule.cpp - rfc/ipv6_address_rule.cpp - rfc/origin_form_rule.cpp - rfc/pchars.cpp - rfc/pct_encoded_rule.cpp - rfc/query_rule.cpp - rfc/relative_ref_rule.cpp - rfc/reserved_chars.cpp - rfc/sub_delim_chars.cpp - rfc/unreserved_chars.cpp - rfc/uri_rule.cpp - rfc/uri_reference_rule.cpp - compat/ada.cpp - ; -for local f in $(SOURCES) +for local f in [ glob-tree-ex . : *.cpp : doc_grammar.cpp doc_3_urls.cpp example ] { run $(f) ; } From 592fec0ec0227b1c682027ac8a9d2c778c541d96 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:07 -0500 Subject: [PATCH 02/21] test: public interface boundary and fuzz tests --- .../public_interfaces_boundaries.cpp | 1026 +++++++++++++++++ .../public_interfaces_fuzz.cpp | 813 +++++++++++++ 2 files changed, 1839 insertions(+) create mode 100644 test/unit/public_interfaces/public_interfaces_boundaries.cpp create mode 100644 test/unit/public_interfaces/public_interfaces_fuzz.cpp diff --git a/test/unit/public_interfaces/public_interfaces_boundaries.cpp b/test/unit/public_interfaces/public_interfaces_boundaries.cpp new file mode 100644 index 000000000..4f8687f70 --- /dev/null +++ b/test/unit/public_interfaces/public_interfaces_boundaries.cpp @@ -0,0 +1,1026 @@ +// test/unit/public_interfaces/public_interfaces_boundaries.cpp +// +// Boundary & edge cases for Boost.URL public interfaces. +// Covers: parse function boundaries, resolve, encode/decode roundtrip, +// construction, modification setters, segments, params, comparison, authority. + +#include +#include +#include + +#include "test_suite.hpp" + +#include +#include // std::strlen +#include // std::size_t +#include // std::cerr +#include // std::move + +namespace boost { +namespace urls { + +namespace { + +// ============================================================ +// Helpers +// ============================================================ + +struct Case +{ + const char* s; + bool uri; + bool uri_ref; + bool rel_ref; + bool origin; +}; + +static const Case kCases[] = { + // empty / minimal + {"", false, true, true, false}, + {"#", false, true, true, false}, + {"?", false, true, true, false}, + {"/", false, true, true, true}, + + // scheme-ish (calibrated to Boost.URL 1.90 behavior) + {"http:", true, true, false, false}, + {"http://", true, true, false, false}, + {"mailto:a@b",true, true, false, false}, + + // authority-ish (calibrated: parse_origin_form accepts these) + {"//example.com", false, true, true, true}, + {"//user:pass@example.com", false, true, true, true}, + {"//[::1]/", false, true, true, false}, + + // typical full URLs + {"https://example.com/", true, true, false, false}, + {"https://example.com/a/b?x=1&y=2#frag", true, true, false, false}, + {"https://user:pass@example.com:443/p?q#f", true, true, false, false}, + + // origin-form (HTTP request target) + {"/path/to/resource?x=1", false, true, true, true}, + {"*", false, true, true, false}, // not accepted by parse_origin_form in this version + {"/%2f%2F", false, true, true, true}, + + // percent escapes: valid + invalid (calibrated: invalid % rejected by uri_reference/relative/origin) + {"https://example.com/%2F", true, true, false, false}, + {"https://example.com/%", false, false, false, false}, + {"https://example.com/%GG", false, false, false, false}, + {"/path?x=%", false, false, false, false}, + + // spaces and controls (calibrated: spaces rejected by uri_reference) + {"https://example.com/has space", false, false,false, false}, + {"\r\n", false, false,false, false}, + + // weird but common in real systems + {"https://example.com/a+b?q=hello+world", true, true, false, false}, +}; + +// Heuristic: does input *likely* contain an invalid pct-encoding sequence? +// We use this only to decide whether decode_view(sv) should throw. +// It's conservative: if we're not sure, we won't require a throw. +bool looks_like_bad_pct(const char* s) +{ + for (std::size_t i = 0; s[i] != '\0'; ++i) + { + if (s[i] != '%') continue; + + // '%' at end or too short -> bad + if (s[i + 1] == '\0' || s[i + 2] == '\0') + return true; + + auto is_hex = [](unsigned char c) { + return (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f'); + }; + + if (!is_hex(static_cast(s[i + 1])) || + !is_hex(static_cast(s[i + 2]))) + return true; + + i += 2; // skip the two hex chars + } + return false; +} + +// Encode helper using stable overload: encode(char*, cap, sv, charset, opts). +std::string encode_to_string(boost::core::string_view sv) +{ + auto const& cs = pchars; + + encoding_opts opt; // default + + std::size_t cap = sv.size() * 3 + 1; // worst-case expansion + std::string out; + out.resize(cap); + + std::size_t n = encode(&out[0], out.size(), sv, cs, opt); + out.resize(n); + return out; +} + +// Single-argument BOOST_TEST harness; provide diagnostics separately. +template +void expect_ok(Result const& r, bool should_succeed, const char* which, const char* s) +{ + bool actual = r.has_value(); + if (actual != should_succeed) + { + std::cerr << which << " expectation mismatch for: [" << s + << "] expected=" << (should_succeed ? "success" : "failure") + << " actual=" << (actual ? "success" : "failure") << "\n"; + } + + BOOST_TEST(actual == should_succeed); + + if (r) + { + // Basic invariants that should never crash. + auto b = r->buffer(); + BOOST_TEST(b.data() != nullptr || b.size() == 0); + } +} + +// Helper: expect operation to succeed +template +void expect_success(const char* desc, Fn fn) +{ + try + { + fn(); + } + catch (std::exception const& e) + { + std::cerr << desc << " failed unexpectedly: " << e.what() << "\n"; + BOOST_TEST(false); + } +} + +// Helper: expect operation to throw +template +void expect_throw(const char* desc, Fn fn) +{ + bool threw = false; + try + { + fn(); + } + catch (boost::system::system_error const&) + { + threw = true; + } + catch (std::exception const& e) + { + std::cerr << desc << " threw unexpected exception type: " << e.what() << "\n"; + } + + if (!threw) + { + std::cerr << desc << " did not throw as expected\n"; + } + BOOST_TEST(threw); +} + +} // namespace + +struct public_interfaces_boundaries_test +{ + void test_parse_boundaries() + { + for (auto const& c : kCases) + { + boost::core::string_view sv(c.s, std::strlen(c.s)); + + expect_ok(parse_uri(sv), c.uri, "parse_uri", c.s); + expect_ok(parse_uri_reference(sv), c.uri_ref, "parse_uri_reference", c.s); + expect_ok(parse_relative_ref(sv), c.rel_ref, "parse_relative_ref", c.s); + expect_ok(parse_origin_form(sv), c.origin, "parse_origin_form", c.s); + + // parse_query / parse_path: robustness checks (no crash). + { + auto rq = parse_query(sv); + if (rq) + { + std::size_t n = 0; + for (auto const& p : *rq) + { + (void)p; + if (++n > 64) break; + } + } + } + { + auto rp = parse_path(sv); + if (rp) + { + std::size_t n = 0; + for (auto const& seg : *rp) + { + (void)seg; + if (++n > 64) break; + } + } + } + + // resolve(base, ref, dest) + { + auto base_r = parse_uri("https://example.com/base/"); + BOOST_TEST(base_r.has_value()); + + auto ref_r = parse_uri_reference(sv); + if (ref_r) + { + url dest; + auto rr = resolve(*base_r, *ref_r, dest); + if (rr) + { + auto b = dest.buffer(); + BOOST_TEST(b.data() != nullptr || b.size() == 0); + } + } + } + + // Percent encoding/decoding boundary checks. + // + // 1) Known-valid pct string produced by encode_to_string must not throw in decode_view. + { + std::string enc = encode_to_string(sv); + + bool threw = false; + try + { + decode_view dv(enc); + (void)dv.size(); + if (!dv.empty()) + { + (void)dv.front(); + (void)dv.back(); + } + + std::string dec; + dec.reserve(dv.size()); + for (char ch : dv) + dec.push_back(ch); + + BOOST_TEST(dec.size() <= enc.size()); + } + catch (boost::system::system_error const& e) + { + threw = true; + std::cerr << "decode_view threw unexpectedly on encoded input for: [" << c.s + << "] message=" << e.what() << "\n"; + } + BOOST_TEST(!threw); + } + + // 2) Direct decode_view on raw input: + // - If it *looks* like it has invalid percent-encoding, we expect a throw. + // - Otherwise, it should not throw. + { + bool expect_throw = looks_like_bad_pct(c.s); + + bool threw = false; + try + { + decode_view dv_bad(sv); + (void)dv_bad.size(); + if (!dv_bad.empty()) + (void)dv_bad.front(); + } + catch (boost::system::system_error const&) + { + threw = true; + } + + BOOST_TEST(threw == expect_throw); + } + } + } + + void test_url_construction() + { + // Default construction + { + url u; + BOOST_TEST(u.empty()); + BOOST_TEST(u.buffer() == ""); + BOOST_TEST(u.size() == 0); + } + + // String construction - valid + { + expect_success("construct full URL", [&] { + url u1("https://example.com/path?q=1#frag"); + BOOST_TEST(u1.scheme() == "https"); + BOOST_TEST(u1.host() == "example.com"); + }); + + expect_success("construct relative path", [&] { + url u2("/relative/path"); + BOOST_TEST(!u2.has_scheme()); + BOOST_TEST(u2.path() == "/relative/path"); + }); + + expect_success("construct query only", [&] { + url u3("?query=only"); + BOOST_TEST(u3.has_query()); + }); + + expect_success("construct fragment only", [&] { + url u4("#fragment-only"); + BOOST_TEST(u4.has_fragment()); + }); + } + + // String construction - invalid (should throw) + { + expect_throw("construct with space in scheme", + [&] { url u("ht tp://example.com"); }); + expect_throw("construct with space in host", + [&] { url u("http://exam ple.com"); }); + expect_throw("construct with invalid pct-encoding", + [&] { url u("http://example.com/%"); }); + } + + // Copy construction + { + url u1("https://example.com/path"); + url u2(u1); + BOOST_TEST(u1 == u2); + BOOST_TEST(u1.buffer() == u2.buffer()); + BOOST_TEST(u1.buffer().data() != u2.buffer().data()); // Different buffers + } + + // Move construction + { + url u1("https://example.com/path"); + url u2(std::move(u1)); + BOOST_TEST(u2.buffer() == "https://example.com/path"); + } + + // Assignment + { + url u1("https://example.com/"); + url u2("http://other.org/"); + u1 = u2; + BOOST_TEST(u1 == u2); + BOOST_TEST(u1.buffer() == "http://other.org/"); + } + + // Self-assignment (via reference to avoid -Wself-assign-overloaded) + { + url u("https://example.com/"); + url& ref = u; + u = ref; + BOOST_TEST(u.buffer() == "https://example.com/"); + } + } + + void test_url_modification_setters() + { + // set_scheme - valid cases + { + url u("http://example.com/path"); + u.set_scheme("https"); + BOOST_TEST(u.buffer() == "https://example.com/path"); + + u.set_scheme("ws"); + BOOST_TEST(u.scheme() == "ws"); + + u.set_scheme("ftp"); + BOOST_TEST(u.scheme() == "ftp"); + } + + // set_scheme - invalid cases (should throw) + { + url u("http://example.com/"); + expect_throw("set_scheme empty", [&] { u.set_scheme(""); }); + expect_throw("set_scheme with space", [&] { u.set_scheme("ht tp"); }); + expect_throw("set_scheme starting with digit", [&] { u.set_scheme("123abc"); }); + expect_throw("set_scheme with colon", [&] { u.set_scheme("http://"); }); + } + + // set_scheme_id + { + url u("http://example.com/"); + u.set_scheme_id(scheme::https); + BOOST_TEST(u.scheme_id() == scheme::https); + BOOST_TEST(u.scheme() == "https"); + } + + // set_user - valid cases + { + url u("http://example.com/"); + u.set_user("alice"); + BOOST_TEST(u.user() == "alice"); + BOOST_TEST(u.encoded_user() == "alice"); + + u.set_user("bob smith"); // Should encode space + BOOST_TEST(u.user() == "bob smith"); + BOOST_TEST(u.encoded_user() == "bob%20smith"); + + u.set_user(""); // Empty user + BOOST_TEST(u.user() == ""); + } + + // set_user - special characters + { + url u("http://example.com/"); + u.set_user("user@domain"); + BOOST_TEST(u.user() == "user@domain"); + BOOST_TEST(u.encoded_user().find("%40") != core::string_view::npos); + } + + // set_password - valid cases + { + url u("http://user@example.com/"); + u.set_password("secret"); + BOOST_TEST(u.password() == "secret"); + + u.set_password("pa$$word"); // Special chars + BOOST_TEST(u.password() == "pa$$word"); + + u.set_password(""); // Empty password + BOOST_TEST(u.password() == ""); + } + + // set_userinfo - combined + { + url u("http://example.com/"); + u.set_userinfo("alice:secret"); + BOOST_TEST(u.user() == "alice"); + BOOST_TEST(u.password() == "secret"); + + u.set_userinfo("bob"); // No password + BOOST_TEST(u.user() == "bob"); + BOOST_TEST(!u.has_password()); + } + + // set_host - valid cases + { + url u("http://example.com/"); + + // Domain name + u.set_host("example.org"); + BOOST_TEST(u.host() == "example.org"); + + // IPv4 address + u.set_host("127.0.0.1"); + BOOST_TEST(u.host() == "127.0.0.1"); + BOOST_TEST(u.host_type() == host_type::ipv4); + + // IPv6 address + u.set_host("[::1]"); + BOOST_TEST(u.host() == "[::1]"); + BOOST_TEST(u.host_type() == host_type::ipv6); + } + + // set_port - valid cases + { + url u("http://example.com/"); + + u.set_port("8080"); + BOOST_TEST(u.port() == "8080"); + BOOST_TEST(u.port_number() == 8080); + + u.set_port("80"); + BOOST_TEST(u.port() == "80"); + } + + // set_port - invalid cases + { + url u("http://example.com/"); + // "99999" is valid per RFC 3986 (port = *DIGIT), no range check + expect_throw("set_port non-numeric", [&] { u.set_port("abc"); }); + expect_throw("set_port negative", [&] { u.set_port("-1"); }); + } + + // set_port_number + { + url u("http://example.com/"); + u.set_port_number(8080); + BOOST_TEST(u.port_number() == 8080); + + u.set_port_number(443); + BOOST_TEST(u.port_number() == 443); + } + + // set_path - valid cases + { + url u("http://example.com/"); + + u.set_path("/new/path"); + BOOST_TEST(u.path() == "/new/path"); + + u.set_path("/path with spaces"); + BOOST_TEST(u.path() == "/path with spaces"); + BOOST_TEST(u.encoded_path().find("%20") != core::string_view::npos); + + u.set_path(""); // Empty path + BOOST_TEST(u.path() == ""); + } + + // set_query - valid cases + { + url u("http://example.com/path"); + + u.set_query("a=1&b=2"); + BOOST_TEST(u.query() == "a=1&b=2"); + + u.set_query("x=hello world"); + BOOST_TEST(u.query() == "x=hello world"); + + u.set_query(""); // Empty query + BOOST_TEST(u.query() == ""); + } + + // set_fragment - valid cases + { + url u("http://example.com/path"); + + u.set_fragment("section1"); + BOOST_TEST(u.fragment() == "section1"); + + u.set_fragment("section with spaces"); + BOOST_TEST(u.fragment() == "section with spaces"); + + u.set_fragment(""); // Empty fragment + BOOST_TEST(u.fragment() == ""); + } + + // Chained modifications + { + url u("http://example.com/"); + u.set_scheme("https") + .set_host("secure.example.org") + .set_port("8443") + .set_path("/api/v1/users") + .set_query("limit=10") + .set_fragment("results"); + + BOOST_TEST(u.scheme() == "https"); + BOOST_TEST(u.host() == "secure.example.org"); + BOOST_TEST(u.port() == "8443"); + BOOST_TEST(u.path() == "/api/v1/users"); + BOOST_TEST(u.has_query()); + BOOST_TEST(u.has_fragment()); + } + + // Removal operations + { + url u("https://user:pass@example.com:8080/path?q=1#frag"); + + u.remove_authority(); + BOOST_TEST(!u.has_authority()); + + u = url("https://user:pass@example.com:8080/path?q=1#frag"); + u.remove_userinfo(); + BOOST_TEST(!u.has_userinfo()); + BOOST_TEST(u.has_authority()); // Still has host + + u.remove_port(); + BOOST_TEST(!u.has_port()); + + u.remove_query(); + BOOST_TEST(!u.has_query()); + + u.remove_fragment(); + BOOST_TEST(!u.has_fragment()); + } + } + + void test_segments_operations() + { + // Basic push_back/pop_back + { + url u("http://example.com/"); + auto segs = u.segments(); + + segs.push_back("api"); + BOOST_TEST(u.path() == "/api"); + + segs.push_back("v1"); + BOOST_TEST(u.path() == "/api/v1"); + + segs.push_back("users"); + BOOST_TEST(u.path() == "/api/v1/users"); + + segs.pop_back(); + BOOST_TEST(u.path() == "/api/v1"); + } + + // Segments with encoding + { + url u("http://example.com/"); + auto segs = u.segments(); + + segs.push_back("path with spaces"); + BOOST_TEST(u.encoded_path().find("%20") != core::string_view::npos); + + segs.push_back("a/b"); // Slash in segment + BOOST_TEST(u.encoded_path().find("%2F") != core::string_view::npos || + u.encoded_path().find("%2f") != core::string_view::npos); + } + + // Insert operations + { + url u("http://example.com/a/c"); + auto segs = u.segments(); + + auto it = segs.begin(); + ++it; // Move to position after 'a' + segs.insert(it, "b"); + BOOST_TEST(u.path() == "/a/b/c"); + + segs.insert(segs.begin(), "start"); + BOOST_TEST(u.path() == "/start/a/b/c"); + + segs.insert(segs.end(), "end"); + BOOST_TEST(u.path() == "/start/a/b/c/end"); + } + + // Erase operations + { + url u("http://example.com/a/b/c/d"); + auto segs = u.segments(); + + auto it = segs.begin(); + ++it; // Point to 'b' + segs.erase(it); // Remove 'b' + BOOST_TEST(u.path() == "/a/c/d"); + + // Erase range + auto it2 = segs.begin(); + auto it3 = segs.begin(); + ++it3; + ++it3; + segs.erase(it2, it3); // Remove first 2 elements + BOOST_TEST(u.path() == "/d"); + } + + // Replace operations + { + url u("http://example.com/old/path/here"); + auto segs = u.segments(); + + segs.replace(segs.begin(), "new"); + BOOST_TEST(u.path() == "/new/path/here"); + + auto it = segs.begin(); + ++it; // Point to second segment + segs.replace(it, "route"); + BOOST_TEST(u.path() == "/new/route/here"); + } + + // Clear + { + url u("http://example.com/a/b/c"); + auto segs = u.segments(); + + segs.clear(); + BOOST_TEST(u.path() == ""); + BOOST_TEST(segs.empty()); + } + + // Empty segments + { + url u("http://example.com/"); + auto segs = u.segments(); + + segs.push_back(""); + segs.push_back("a"); + segs.push_back(""); + BOOST_TEST(segs.size() == 3); + } + + // Large number of segments (stress test) + { + url u("http://example.com/"); + auto segs = u.segments(); + + for (int i = 0; i < 100; ++i) { + segs.push_back(std::to_string(i)); + } + + BOOST_TEST(segs.size() == 100); + BOOST_TEST(segs.front() == "0"); + BOOST_TEST(segs.back() == "99"); + } + } + + void test_params_operations() + { + // Basic append + { + url u("http://example.com/path"); + auto params = u.params(); + + params.append(param_view{"a", "1"}); + BOOST_TEST(u.query() == "a=1"); + + params.append(param_view{"b", "2"}); + BOOST_TEST(u.query() == "a=1&b=2"); + + params.append(param_view{"c", "3"}); + BOOST_TEST(u.query() == "a=1&b=2&c=3"); + } + + // Params with encoding + { + url u("http://example.com/"); + auto params = u.params(); + + params.append(param_view{"key", "value with spaces"}); + // Spaces in query params are encoded as '+' (application/x-www-form-urlencoded) + BOOST_TEST( + u.encoded_query().find("%20") != core::string_view::npos || + u.encoded_query().find("+") != core::string_view::npos); + + params.append(param_view{"key&name", "val=ue"}); + } + + // Insert operations + { + url u("http://example.com/?a=1&c=3"); + auto params = u.params(); + + auto it = params.find("c"); + params.insert(it, param_view{"b", "2"}); + BOOST_TEST(u.query() == "a=1&b=2&c=3"); + } + + // Erase operations + { + url u("http://example.com/?a=1&b=2&c=3"); + auto params = u.params(); + + auto it = params.find("b"); + params.erase(it); + BOOST_TEST(u.query() == "a=1&c=3"); + + params.erase(params.begin()); + BOOST_TEST(u.query() == "c=3"); + } + + // Replace operations + { + url u("http://example.com/?a=1&b=2"); + auto params = u.params(); + + auto it = params.find("a"); + params.replace(it, param_view{"a", "10"}); + + auto it2 = params.find("a"); + BOOST_TEST(it2 != params.end()); + BOOST_TEST((*it2).value == "10"); + } + + // Clear + { + url u("http://example.com/?a=1&b=2&c=3"); + auto params = u.params(); + + params.clear(); + BOOST_TEST(!u.has_query()); + BOOST_TEST(params.empty()); + } + + // Duplicate keys + { + url u("http://example.com/"); + auto params = u.params(); + + params.append(param_view{"key", "value1"}); + params.append(param_view{"key", "value2"}); + params.append(param_view{"key", "value3"}); + + int count = 0; + for (auto const& p : params) { + if (p.key == "key") ++count; + } + BOOST_TEST(count == 3); + + auto it = params.find("key"); + BOOST_TEST(it != params.end()); + BOOST_TEST((*it).value == "value1"); + } + + // Count operation + { + url u("http://example.com/?k=1&k=2&k=3&x=4"); + auto params = u.params(); + + BOOST_TEST(params.count("k") == 3); + BOOST_TEST(params.count("x") == 1); + BOOST_TEST(params.count("missing") == 0); + } + + // Empty keys and values + { + url u("http://example.com/"); + auto params = u.params(); + + params.append(param_view{"", "empty-key"}); + params.append(param_view{"empty-value", ""}); + params.append(param_view{"", ""}); + + BOOST_TEST(params.size() == 3); + } + + // Large number of params (stress test) + { + url u("http://example.com/"); + auto params = u.params(); + + for (int i = 0; i < 100; ++i) { + std::string key = std::to_string(i); + std::string val = std::to_string(i * 10); + params.append(param_view{key, val}); + } + + BOOST_TEST(params.size() == 100); + } + } + + void test_url_comparison() + { + // Exact equality + { + url u1("https://example.com/path?q=1#frag"); + url u2("https://example.com/path?q=1#frag"); + BOOST_TEST(u1 == u2); + BOOST_TEST(!(u1 != u2)); + } + + // Different encoding but same bytes + { + url u1("https://example.com/path%20here"); + url u2("https://example.com/path%20here"); + BOOST_TEST(u1 == u2); + } + + // Path case sensitivity + { + url u3("http://example.com/Path"); + url u4("http://example.com/path"); + BOOST_TEST(u3 != u4); + } + + // Ordering (operator<) + { + url u1("http://a.com/"); + url u2("http://b.com/"); + BOOST_TEST((u1 < u2) || (u2 < u1)); + + url u3("http://example.com/a"); + url u4("http://example.com/b"); + BOOST_TEST(u3 < u4); + } + + // Self-comparison + { + url u("https://example.com/"); + BOOST_TEST(u == u); + BOOST_TEST(!(u != u)); + BOOST_TEST(!(u < u)); + } + + // Empty URLs + { + url u1; + url u2; + BOOST_TEST(u1 == u2); + } + + // Different components + { + url u1("https://example.com/path"); + url u2("http://example.com/path"); + BOOST_TEST(u1 != u2); + + url u3("https://example.com/path"); + url u4("https://example.org/path"); + BOOST_TEST(u3 != u4); + + url u5("https://example.com/path1"); + url u6("https://example.com/path2"); + BOOST_TEST(u5 != u6); + } + } + + void test_authority_components() + { + // IPv4 addresses + { + url u1("http://127.0.0.1/"); + BOOST_TEST(u1.host_type() == host_type::ipv4); + BOOST_TEST(u1.host() == "127.0.0.1"); + + auto ipv4 = u1.host_ipv4_address(); + BOOST_TEST(ipv4.to_string() == "127.0.0.1"); + + url u2("http://192.168.1.1:8080/"); + BOOST_TEST(u2.host_ipv4_address().to_string() == "192.168.1.1"); + } + + // IPv4 edge cases + { + url u1("http://0.0.0.0/"); + BOOST_TEST(u1.host_type() == host_type::ipv4); + + url u2("http://255.255.255.255/"); + BOOST_TEST(u2.host_type() == host_type::ipv4); + } + + // IPv6 addresses + { + url u1("http://[::1]/"); + BOOST_TEST(u1.host_type() == host_type::ipv6); + BOOST_TEST(u1.host() == "[::1]"); + + auto ipv6 = u1.host_ipv6_address(); + BOOST_TEST(ipv6.is_loopback()); + + url u2("http://[2001:db8::1]:8080/"); + BOOST_TEST(u2.host_type() == host_type::ipv6); + } + + // IPv6 edge cases + { + url u1("http://[::]/"); + BOOST_TEST(u1.host_type() == host_type::ipv6); + } + + // Domain names + { + url u1("http://example.com/"); + BOOST_TEST(u1.host_type() == host_type::name); + BOOST_TEST(u1.host() == "example.com"); + + url u2("http://sub.example.co.uk/"); + BOOST_TEST(u2.host_type() == host_type::name); + } + + // Port extraction + { + url u1("http://example.com:8080/"); + BOOST_TEST(u1.has_port()); + BOOST_TEST(u1.port() == "8080"); + BOOST_TEST(u1.port_number() == 8080); + + url u2("http://example.com/"); + BOOST_TEST(!u2.has_port()); + } + + // Port edge cases + { + url u1("http://example.com:1/"); + BOOST_TEST(u1.port_number() == 1); + + url u2("http://example.com:65535/"); + BOOST_TEST(u2.port_number() == 65535); + } + + // Userinfo extraction + { + url u1("http://user@example.com/"); + BOOST_TEST(u1.has_userinfo()); + BOOST_TEST(u1.user() == "user"); + BOOST_TEST(!u1.has_password()); + + url u2("http://user:pass@example.com/"); + BOOST_TEST(u2.has_userinfo()); + BOOST_TEST(u2.user() == "user"); + BOOST_TEST(u2.has_password()); + BOOST_TEST(u2.password() == "pass"); + } + + // Authority view + { + url u("http://user:pass@example.com:8080/"); + auto auth = u.authority(); + + BOOST_TEST(auth.buffer() == "user:pass@example.com:8080"); + BOOST_TEST(auth.user() == "user"); + BOOST_TEST(auth.password() == "pass"); + BOOST_TEST(auth.host() == "example.com"); + BOOST_TEST(auth.port() == "8080"); + } + } + + void run() + { + test_parse_boundaries(); + test_url_construction(); + test_url_modification_setters(); + test_segments_operations(); + test_params_operations(); + test_url_comparison(); + test_authority_components(); + } +}; + +TEST_SUITE(public_interfaces_boundaries_test, "boost.url.public_interfaces_boundaries"); + +} // urls +} // boost diff --git a/test/unit/public_interfaces/public_interfaces_fuzz.cpp b/test/unit/public_interfaces/public_interfaces_fuzz.cpp new file mode 100644 index 000000000..37a694776 --- /dev/null +++ b/test/unit/public_interfaces/public_interfaces_fuzz.cpp @@ -0,0 +1,813 @@ +// test/unit/public_interfaces/public_interfaces_fuzz.cpp +// +// Deterministic fuzz-style tests for Boost.URL public interfaces. +// +// This file intentionally avoids libFuzzer integration and instead performs +// bounded randomized stress testing. +// +// Phases: +// - Parse/resolve/encode/decode fuzzing with mutation +// - Construction fuzzing +// - Comparison fuzzing +// - Container (segments/params) stress +// - Authority component fuzzing +// - Normalize/resolve fuzzing +// - Copy/move fuzzing +// - Encode buffer boundary abuse + +#include +#include +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include +#include // std::strlen +#include +#include +#include +#include // std::cerr +#include // std::size_t +#include // std::move + +namespace boost { +namespace urls { + +namespace { + +// ============================================================ +// Shared helpers +// ============================================================ + +static const char* const kSeeds[] = { + "https://example.com/", + "https://user:pass@example.com:443/path/to/file.txt?x=1&y=2#frag", + "http://127.0.0.1:8080/a/b?c=d", + "mailto:someone@example.com", + "urn:uuid:123e4567-e89b-12d3-a456-426614174000", + "/just/a/path?and=query#frag", + "//example.com/authority-only", + "https://example.com/%2Fencoded%20space?q=a+b", + "ws://example.com/chat", + "file:///C:/Windows/System32/drivers/etc/hosts" +}; + +std::string make_random_input(std::mt19937& rng, std::size_t max_len) +{ + static const char kAlphabet[] = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "-._~" + ":/?#[]@" + "!$&'()*+,;=" + "% " + "\"<>\\^`{|}\t\r\n"; + + std::uniform_int_distribution len_dist(0, max_len); + std::uniform_int_distribution ch_dist(0, sizeof(kAlphabet) - 2); + std::uniform_int_distribution coin(0, 999); + + std::size_t n = len_dist(rng); + std::string s; + s.reserve(n); + + for (std::size_t i = 0; i < n; ++i) + { + // Rarely inject embedded NUL + if (coin(rng) == 0) + { + s.push_back('\0'); + continue; + } + + char c = kAlphabet[ch_dist(rng)]; + s.push_back(c); + + // Occasionally inject a percent-escape (valid or invalid). + if (c == '%' && i + 2 < n) + { + static const char kHex[] = "0123456789ABCDEFabcdefGg"; + std::uniform_int_distribution hx_dist(0, sizeof(kHex) - 2); + s.push_back(kHex[hx_dist(rng)]); + s.push_back(kHex[hx_dist(rng)]); + i += 2; + } + } + + return s; +} + +// Most-compatible encode helper: buffer form +std::string encode_to_string(boost::core::string_view sv) +{ + auto const& cs = pchars; + + encoding_opts opt; + + std::size_t cap = sv.size() * 3 + 1; // worst-case expansion + std::string out; + out.resize(cap); + + std::size_t n = encode(&out[0], out.size(), sv, cs, opt); + out.resize(n); + return out; +} + +template +void cheap_invariants(UrlViewLike const& u) +{ + auto b = u.buffer(); + BOOST_TEST(b.data() != nullptr || b.size() == 0); + (void)u.size(); +} + +// Exercise iterator-heavy views (good at surfacing internal offset bugs). +void exercise_views(url const& u) +{ + try + { + auto p = u.params(); + std::size_t n = 0; + for (auto it = p.begin(); it != p.end() && n < 16; ++it, ++n) + (void)*it; + } + catch (...) {} + + try + { + auto s = u.segments(); + std::size_t n = 0; + for (auto it = s.begin(); it != s.end() && n < 16; ++it, ++n) + (void)*it; + } + catch (...) {} +} + +// Aggressive mutation: 41 distinct operations covering setters, encoded +// setters, params, segments, remove, clear, swap, normalize, assignment. +void mutate_url(url& u, std::mt19937& rng) +{ + std::uniform_int_distribution op_dist(0, 40); + + auto rand_text = [&]() -> std::string { + static const char* texts[] = { + "", "a", "x", "test", "hello world", + "a+b", "a/b", "a%20b", "a%", "a%GG" + }; + std::uniform_int_distribution d(0, 9); + return texts[d(rng)]; + }; + + int op = op_dist(rng); + + try { + switch (op) { + case 0: u.set_scheme("http"); break; + case 1: u.set_scheme("https"); break; + case 2: u.set_user("user"); break; + case 3: u.set_password("pass"); break; + case 4: u.set_host("example.com"); break; + case 5: u.set_host("127.0.0.1"); break; + case 6: u.set_port("80"); break; + case 7: u.set_port(""); break; + case 8: u.set_path("/"); break; + case 9: u.set_path(std::string("/") + rand_text() + "/" + rand_text()); break; + case 10: u.set_query(std::string("k=") + rand_text() + "&x=" + rand_text()); break; + case 11: u.set_fragment(rand_text()); break; + case 12: { + auto p = u.params(); + std::string val = rand_text(); + p.append(param_view{"k", val}); + break; + } + case 13: { + auto p = u.params(); + auto it = p.find("k"); + if (it != p.end()) + p.erase(it); + std::string val = rand_text(); + p.append(param_view{"k", val}); + break; + } + case 14: { + auto s = u.segments(); + s.push_back(rand_text()); + break; + } + case 15: { + auto s = u.segments(); + if (!s.empty()) + s.pop_back(); + break; + } + case 16: u.set_scheme_id(static_cast(rng() % 10)); break; + case 17: u.set_userinfo(rand_text()); break; + case 18: u.set_encoded_user(rand_text()); break; + case 19: u.set_encoded_password(rand_text()); break; + case 20: u.set_encoded_host(rand_text()); break; + case 21: u.set_port_number(rng() % 65536); break; + case 22: u.set_encoded_path(rand_text()); break; + case 23: u.set_encoded_query(rand_text()); break; + case 24: u.set_encoded_fragment(rand_text()); break; + case 25: u.remove_authority(); break; + case 26: u.remove_userinfo(); break; + case 27: u.remove_port(); break; + case 28: u.remove_query(); break; + case 29: u.remove_fragment(); break; + case 30: { + auto p = u.params(); + if (!p.empty()) { + std::uniform_int_distribution idx(0, p.size() - 1); + auto it = p.begin(); + std::advance(it, idx(rng)); + p.erase(it); + } + break; + } + case 31: { + auto p = u.params(); + p.clear(); + break; + } + case 32: { + auto p = u.params(); + if (!p.empty()) { + std::uniform_int_distribution idx(0, p.size() - 1); + auto it = p.begin(); + std::advance(it, idx(rng)); + std::string key = rand_text(); + std::string val = rand_text(); + p.replace(it, param_view{key, val}); + } + break; + } + case 33: { + auto s = u.segments(); + if (!s.empty()) { + std::uniform_int_distribution idx(0, s.size() - 1); + auto it = s.begin(); + std::advance(it, idx(rng)); + s.erase(it); + } + break; + } + case 34: { + auto s = u.segments(); + s.clear(); + break; + } + case 35: { + auto s = u.segments(); + if (!s.empty()) { + std::uniform_int_distribution idx(0, s.size() - 1); + auto it = s.begin(); + std::advance(it, idx(rng)); + s.insert(it, rand_text()); + } + break; + } + case 36: u.clear(); break; + case 37: u.reserve(rng() % 1000); break; + case 38: { + url u2("http://other.com/path"); + u = u2; + break; + } + case 39: { + url u2; + std::swap(u, u2); + break; + } + case 40: { + try { u.normalize(); } catch(...) {} + break; + } + default: break; + } + } catch (...) { + // Expected under mutation fuzz + } +} + +// Abuse encode buffer sizes (bounds checking). +void encode_bounds_abuse(boost::core::string_view sv) +{ + auto const& cs = pchars; + encoding_opts opt; + + // encoded_size: just compute the size, no output buffer needed + (void)encode(nullptr, 0, {}, cs, opt); + + // small buffers: encode truncates to fit + char buf1[1] = {0}; + char buf8[8] = {0}; + char buf32[32] = {0}; + + (void)encode(buf1, sizeof(buf1), sv, cs, opt); + (void)encode(buf8, sizeof(buf8), sv, cs, opt); + (void)encode(buf32, sizeof(buf32), sv, cs, opt); + + (void)buf1[0]; +} + +// ============================================================ +// Fuzz phases +// ============================================================ + +void fuzz_parse_resolve_encode(std::mt19937& rng, std::size_t iterations) +{ + // Seed corpus first + for (auto* seed : kSeeds) + { + boost::core::string_view sv(seed, std::strlen(seed)); + + auto r1 = parse_uri(sv); + if (r1) cheap_invariants(*r1); + + auto r2 = parse_uri_reference(sv); + if (r2) cheap_invariants(*r2); + + auto r3 = parse_relative_ref(sv); + if (r3) cheap_invariants(*r3); + + auto r4 = parse_origin_form(sv); + if (r4) cheap_invariants(*r4); + + (void)parse_query(sv); + (void)parse_path(sv); + + encode_bounds_abuse(sv); + } + + // Random fuzz-style loop + for (std::size_t i = 0; i < iterations; ++i) + { + std::string s = make_random_input(rng, 768); + boost::core::string_view sv(s.data(), s.size()); + + auto ru = parse_uri(sv); + if (ru) cheap_invariants(*ru); + + auto rur = parse_uri_reference(sv); + if (rur) cheap_invariants(*rur); + + auto rrel = parse_relative_ref(sv); + if (rrel) cheap_invariants(*rrel); + + auto ro = parse_origin_form(sv); + if (ro) cheap_invariants(*ro); + + auto rq = parse_query(sv); + if (rq) + { + std::size_t count = 0; + for (auto it = rq->begin(); it != rq->end() && count < 32; ++it, ++count) + (void)*it; + } + + auto rp = parse_path(sv); + if (rp) + { + std::size_t count = 0; + for (auto it = rp->begin(); it != rp->end() && count < 32; ++it, ++count) + (void)*it; + } + + // resolve(base, ref, dest) + mutation fuzz on resulting url + { + auto base_r = parse_uri("https://example.com/base/path?x=1"); + BOOST_TEST(base_r.has_value()); + + auto ref_r = parse_uri_reference(sv); + if (ref_r) + { + url dest; + auto rr = resolve(*base_r, *ref_r, dest); + if (rr) + { + auto b = dest.buffer(); + BOOST_TEST(b.data() != nullptr || b.size() == 0); + + exercise_views(dest); + + std::uniform_int_distribution steps_dist(5, 40); + int steps = steps_dist(rng); + for (int k = 0; k < steps; ++k) + { + mutate_url(dest, rng); + exercise_views(dest); + + auto enc = encode_to_string(dest.buffer()); + decode_view dv(enc); + + (void)dv.size(); + if (!dv.empty()) + { + (void)dv.front(); + (void)dv.back(); + } + + std::string dec; + dec.reserve(dv.size()); + for (char ch : dv) + dec.push_back(ch); + + BOOST_TEST(dec.size() <= enc.size()); + + encode_bounds_abuse(dest.buffer()); + } + } + } + } + + // Encode/decode stress on raw input + { + std::string enc = encode_to_string(sv); + decode_view dv(enc); + + (void)dv.size(); + if (!dv.empty()) + { + (void)dv.front(); + (void)dv.back(); + } + + std::string dec; + dec.reserve(dv.size()); + for (char ch : dv) + dec.push_back(ch); + + BOOST_TEST(dec.size() <= enc.size()); + } + + encode_bounds_abuse(sv); + } +} + +void fuzz_construction(std::mt19937& rng, std::size_t iterations) +{ + for (std::size_t i = 0; i < iterations; ++i) { + std::string input = make_random_input(rng, 512); + boost::core::string_view sv(input.data(), input.size()); + + try { + url u1(sv); + cheap_invariants(u1); + } catch (...) {} + + auto r = parse_uri_reference(sv); + if (r) { + try { + url u2(*r); + cheap_invariants(u2); + BOOST_TEST(u2 == *r); + } catch (...) {} + } + } +} + +void fuzz_comparison(std::mt19937& rng, std::size_t iterations) +{ + for (std::size_t i = 0; i < iterations; ++i) { + std::string s1 = make_random_input(rng, 256); + std::string s2 = make_random_input(rng, 256); + + auto r1 = parse_uri_reference(s1); + auto r2 = parse_uri_reference(s2); + + if (r1 && r2) { + try { + url u1(*r1); + url u2(*r2); + + bool eq = (u1 == u2); + bool ne = (u1 != u2); + BOOST_TEST(eq != ne); + + bool lt = (u1 < u2); + bool gt = (u2 < u1); + if (eq) { + BOOST_TEST(!lt && !gt); + } + + BOOST_TEST(u1 == u1); + BOOST_TEST(!(u1 != u1)); + BOOST_TEST(!(u1 < u1)); + + } catch (...) {} + } + } +} + +void fuzz_containers(std::mt19937& rng, std::size_t iterations) +{ + for (std::size_t i = 0; i < iterations; ++i) { + url u("http://example.com/"); + + // Stress segments + { + auto segs = u.segments(); + std::uniform_int_distribution op_dist(0, 5); + std::uniform_int_distribution count_dist(1, 20); + + int ops = count_dist(rng); + for (int j = 0; j < ops; ++j) { + try { + int op = op_dist(rng); + auto rand_seg = make_random_input(rng, 32); + + switch (op) { + case 0: segs.push_back(rand_seg); break; + case 1: if (!segs.empty()) segs.pop_back(); break; + case 2: + if (!segs.empty()) { + std::uniform_int_distribution idx(0, segs.size() - 1); + auto it = segs.begin(); + std::advance(it, idx(rng)); + segs.insert(it, rand_seg); + } + break; + case 3: + if (!segs.empty()) { + std::uniform_int_distribution idx(0, segs.size() - 1); + auto it = segs.begin(); + std::advance(it, idx(rng)); + segs.erase(it); + } + break; + case 4: + if (!segs.empty()) { + std::uniform_int_distribution idx(0, segs.size() - 1); + auto it = segs.begin(); + std::advance(it, idx(rng)); + segs.replace(it, rand_seg); + } + break; + case 5: segs.clear(); break; + } + + cheap_invariants(u); + + } catch (...) {} + } + } + + // Stress params + { + auto params = u.params(); + std::uniform_int_distribution op_dist(0, 4); + std::uniform_int_distribution count_dist(1, 20); + + int ops = count_dist(rng); + for (int j = 0; j < ops; ++j) { + try { + int op = op_dist(rng); + auto rand_key = make_random_input(rng, 16); + auto rand_val = make_random_input(rng, 32); + + switch (op) { + case 0: params.append(param_view{rand_key, rand_val}); break; + case 1: + if (!params.empty()) { + std::uniform_int_distribution idx(0, params.size() - 1); + auto it = params.begin(); + std::advance(it, idx(rng)); + params.erase(it); + } + break; + case 2: + if (!params.empty()) { + std::uniform_int_distribution idx(0, params.size() - 1); + auto it = params.begin(); + std::advance(it, idx(rng)); + params.insert(it, param_view{rand_key, rand_val}); + } + break; + case 3: + if (!params.empty()) { + std::uniform_int_distribution idx(0, params.size() - 1); + auto it = params.begin(); + std::advance(it, idx(rng)); + params.replace(it, param_view{rand_key, rand_val}); + } + break; + case 4: params.clear(); break; + } + + cheap_invariants(u); + + } catch (...) {} + } + } + } +} + +void fuzz_authority_components(std::mt19937& rng, std::size_t iterations) +{ + for (std::size_t i = 0; i < iterations; ++i) { + url u("http://example.com/"); + + std::uniform_int_distribution op_dist(0, 10); + + try { + switch (op_dist(rng)) { + case 0: { + std::uniform_int_distribution octet(0, 255); + std::string ipv4 = std::to_string(octet(rng)) + "." + + std::to_string(octet(rng)) + "." + + std::to_string(octet(rng)) + "." + + std::to_string(octet(rng)); + u.set_host(ipv4); + if (u.host_type() == host_type::ipv4) { + cheap_invariants(u); + } + break; + } + case 1: { + std::string ipv6 = "["; + std::uniform_int_distribution hex_dist(0, 0xFFFF); + for (int j = 0; j < 8; ++j) { + if (j > 0) ipv6 += ":"; + std::stringstream ss; + ss << std::hex << hex_dist(rng); + ipv6 += ss.str(); + } + ipv6 += "]"; + u.set_host(ipv6); + if (u.host_type() == host_type::ipv6) { + cheap_invariants(u); + } + break; + } + case 2: { + auto domain = make_random_input(rng, 64); + u.set_host(domain); + break; + } + case 3: { + std::uniform_int_distribution port_dist(1, 65535); + u.set_port_number(port_dist(rng)); + break; + } + case 4: { + auto user = make_random_input(rng, 32); + u.set_user(user); + break; + } + case 5: { + auto pass = make_random_input(rng, 32); + u.set_password(pass); + break; + } + case 6: { + auto userinfo = make_random_input(rng, 64); + u.set_userinfo(userinfo); + break; + } + case 7: u.remove_authority(); break; + case 8: u.remove_userinfo(); break; + case 9: u.remove_port(); break; + case 10: { + if (u.has_authority()) { + auto auth = u.authority(); + cheap_invariants(auth); + + if (u.host_type() == host_type::ipv4) { + auto ipv4 = u.host_ipv4_address(); + (void)ipv4.to_string(); + } else if (u.host_type() == host_type::ipv6) { + auto ipv6 = u.host_ipv6_address(); + (void)ipv6.to_string(); + } + } + break; + } + } + + cheap_invariants(u); + + } catch (...) {} + } +} + +void fuzz_mutation(std::mt19937& rng, std::size_t iterations) +{ + for (std::size_t i = 0; i < iterations; ++i) { + url u("https://example.com/path/to/resource?x=1&y=2#section"); + + std::uniform_int_distribution steps_dist(5, 30); + int steps = steps_dist(rng); + + for (int j = 0; j < steps; ++j) { + mutate_url(u, rng); + cheap_invariants(u); + exercise_views(u); + } + } +} + +void fuzz_normalize_resolve(std::mt19937& rng, std::size_t iterations) +{ + static const char* bases[] = { + "http://example.com/", + "https://example.com/a/b/c", + "ftp://ftp.example.org/dir/", + "http://192.168.1.1:8080/api/", + }; + + for (std::size_t i = 0; i < iterations; ++i) { + std::uniform_int_distribution base_dist(0, 3); + auto base_r = parse_uri(bases[base_dist(rng)]); + + if (!base_r) continue; + + std::string ref_str = make_random_input(rng, 256); + auto ref_r = parse_uri_reference(ref_str); + + if (!ref_r) continue; + + url dest; + auto res = resolve(*base_r, *ref_r, dest); + + if (res) { + cheap_invariants(dest); + exercise_views(dest); + + std::uniform_int_distribution steps_dist(5, 20); + int steps = steps_dist(rng); + for (int k = 0; k < steps; ++k) { + mutate_url(dest, rng); + cheap_invariants(dest); + exercise_views(dest); + } + + try { + dest.normalize(); + cheap_invariants(dest); + } catch (...) {} + } + } +} + +void fuzz_copy_move(std::mt19937& rng, std::size_t iterations) +{ + for (std::size_t i = 0; i < iterations; ++i) { + std::string s = make_random_input(rng, 256); + auto r = parse_uri_reference(s); + + if (!r) continue; + + try { + url u1(*r); + + url u2(u1); + BOOST_TEST(u1 == u2); + cheap_invariants(u2); + + url u3; + u3 = u1; + BOOST_TEST(u1 == u3); + cheap_invariants(u3); + + url u4(std::move(u1)); + cheap_invariants(u4); + + url u5; + u5 = std::move(u2); + cheap_invariants(u5); + + url& ref = u5; + u5 = ref; + cheap_invariants(u5); + + } catch (...) {} + } +} + +} // namespace + +struct public_interfaces_fuzz_test +{ + void run() + { + std::mt19937 rng(0xC0FFEEu); + + fuzz_parse_resolve_encode(rng, 3000); + fuzz_construction(rng, 500); + fuzz_comparison(rng, 500); + fuzz_containers(rng, 200); + fuzz_authority_components(rng, 500); + fuzz_mutation(rng, 300); + fuzz_normalize_resolve(rng, 300); + fuzz_copy_move(rng, 500); + } +}; + +TEST_SUITE(public_interfaces_fuzz_test, "boost.url.public_interfaces_fuzz"); + +} // urls +} // boost From f0a80adb60eb2395a64778844240c8e0a45ecdc7 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:08 -0500 Subject: [PATCH 03/21] fix: encode() UB pointer arithmetic for small buffers --- include/boost/url/impl/encode.hpp | 5 ++--- test/unit/encode.cpp | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/include/boost/url/impl/encode.hpp b/include/boost/url/impl/encode.hpp index 3b43f892d..7bcc799d7 100644 --- a/include/boost/url/impl/encode.hpp +++ b/include/boost/url/impl/encode.hpp @@ -132,7 +132,6 @@ encode( auto const end = dest + size; auto const last = it + s.size(); auto const dest0 = dest; - auto const end3 = end - 3; if (!opt.space_as_plus) { @@ -147,7 +146,7 @@ encode( ++it; continue; } - if (dest > end3) + if (end - dest < 3) return dest - dest0; encode(dest, c); ++it; @@ -177,7 +176,7 @@ encode( ++it; continue; } - if(dest > end3) + if(end - dest < 3) return dest - dest0; encode(dest, c); ++it; diff --git a/test/unit/encode.cpp b/test/unit/encode.cpp index 58736cdf3..b4954064d 100644 --- a/test/unit/encode.cpp +++ b/test/unit/encode.cpp @@ -196,11 +196,25 @@ class encode_test } } + void + testEncodeZeroDest() + { + // encode() with zero-size dest buffer + // must not perform UB pointer arithmetic + { + char buf[1] = {}; + std::size_t n = encode( + buf, 0, "test", pchars); + BOOST_TEST_EQ(n, 0u); + } + } + void run() { testEncode(); testEncodeExtras(); + testEncodeZeroDest(); testJavadocs(); } }; From af227d47f7ebd3fa9f6432e079d935cb1fcd4db3 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:08 -0500 Subject: [PATCH 04/21] fix: url_base loop condition order --- include/boost/url/impl/url_base.hpp | 12 +++++++----- test/unit/url_base.cpp | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/include/boost/url/impl/url_base.hpp b/include/boost/url/impl/url_base.hpp index 3aa9ae5af..8edfd3cb4 100644 --- a/include/boost/url/impl/url_base.hpp +++ b/include/boost/url/impl/url_base.hpp @@ -245,8 +245,8 @@ remove_scheme() auto begin = s_ + impl_.offset(id_path); auto it = begin; auto end = begin + pn; - while (*it != '/' && - it != end) + while (it != end && + *it != '/') ++it; // we don't need op here because this is // an internal operation @@ -2089,8 +2089,8 @@ normalize_path() auto end = begin + pn; while (core::string_view(it, 2) == "./") it += 2; - while (*it != '/' && - it != end) + while (it != end && + *it != '/') ++it; // we don't need op here because this is // an internal operation @@ -2614,8 +2614,10 @@ first_segment() const noexcept p0, end - p0); auto p = p0; while(*p != '/') + { + BOOST_ASSERT(p < end); ++p; - BOOST_ASSERT(p < end); + } return core::string_view(p0, p - p0); } diff --git a/test/unit/url_base.cpp b/test/unit/url_base.cpp index 404c8647e..a352ef91c 100644 --- a/test/unit/url_base.cpp +++ b/test/unit/url_base.cpp @@ -1860,6 +1860,23 @@ struct url_base_test assert( url( "http://www.example.com#%61bc" ).normalize_fragment().buffer() == "http://www.example.com#abc" ); } + void + testSetEncodedPathBoundary() + { + // exercise paths that end exactly at + // iterator boundary (OOB read regression) + { + url u; + u.set_encoded_path("x"); + BOOST_TEST_EQ(u.encoded_path(), "x"); + } + { + url u; + u.set_encoded_path(""); + BOOST_TEST(u.encoded_path().empty()); + } + } + void run() { @@ -1871,6 +1888,7 @@ struct url_base_test testSetHost(); testSetPort(); testQuery(); + testSetEncodedPathBoundary(); testJavadocs(); } }; From 61c896201bd2091a148ea0b28f06afb985fbe44b Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:08 -0500 Subject: [PATCH 05/21] fix: LLONG_MIN negation UB in format --- src/detail/format_args.cpp | 46 ++++++++++++++++++++------------------ test/unit/format.cpp | 13 +++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/detail/format_args.cpp b/src/detail/format_args.cpp index c43f51c69..e1659ce02 100644 --- a/src/detail/format_args.cpp +++ b/src/detail/format_args.cpp @@ -364,21 +364,24 @@ measure( { dn += measure_one('-', cs); ++n; - v *= -1; } else if (sign != '-') { dn += measure_one(sign, cs); ++n; } + // Use unsigned to avoid UB when v == LLONG_MIN + unsigned long long int uv = v < 0 + ? 0ull - static_cast(v) + : static_cast(v); do { - int d = v % 10; - v /= 10; + int d = static_cast(uv % 10); + uv /= 10; dn += measure_one('0' + static_cast(d), cs); ++n; } - while (v > 0); + while (uv > 0); std::size_t w = width; if (width_idx != std::size_t(-1) || @@ -445,29 +448,29 @@ format( grammar::lut_chars const& cs) const { // get n digits - long long int v0 = v; - long long int p = 1; + // Use unsigned to avoid UB when v == LLONG_MIN + bool const neg = v < 0; + unsigned long long int uv = neg + ? 0ull - static_cast(v) + : static_cast(v); + unsigned long long int uv0 = uv; + unsigned long long int p = 1; std::size_t n = 0; - if (v < 0) - { - v *= - 1; - ++n; - } - else if (sign != '-') + if (neg || sign != '-') { ++n; } do { - if (v >= 10) + if (uv >= 10) p *= 10; - v /= 10; + uv /= 10; ++n; } - while (v > 0); + while (uv > 0); static constexpr auto m = std::numeric_limits::digits10; - BOOST_ASSERT(n <= m + 1); + BOOST_ASSERT(n <= m + 2); ignore_unused(m); // get pad @@ -506,17 +509,16 @@ format( } // write - v = v0; + uv = uv0; char* out = ctx.out(); if (!zeros) { for (std::size_t i = 0; i < lpad; ++i) encode_one(out, fill, cs); } - if (v < 0) + if (neg) { encode_one(out, '-', cs); - v *= -1; --n; } else if (sign != '-') @@ -531,10 +533,10 @@ format( } while (n) { - unsigned long long int d = v / p; + unsigned long long int d = uv / p; encode_one(out, '0' + static_cast(d), cs); --n; - v %= p; + uv %= p; p /= 10; } if (!zeros) @@ -570,7 +572,7 @@ grammar::lut_chars const& cs) const while (v > 0); static constexpr auto m = std::numeric_limits::digits10; - BOOST_ASSERT(n <= m + 1); + BOOST_ASSERT(n <= m + 2); ignore_unused(m); // get pad diff --git a/test/unit/format.cpp b/test/unit/format.cpp index 48409b548..1d16254cd 100644 --- a/test/unit/format.cpp +++ b/test/unit/format.cpp @@ -15,6 +15,8 @@ #include "test_suite.hpp" +#include + #ifdef BOOST_TEST_CSTR_EQ #undef BOOST_TEST_CSTR_EQ #define BOOST_TEST_CSTR_EQ(expr1,expr2) \ @@ -920,6 +922,16 @@ struct format_test } + void + testLLONGMIN() + { + // LLONG_MIN negation must not trigger UB + { + url u = urls::format("/{}/", LLONG_MIN); + BOOST_TEST(u.encoded_path().size() > 0); + } + } + void run() { @@ -928,6 +940,7 @@ struct format_test // without help from the pros. #if !BOOST_WORKAROUND( BOOST_GCC_VERSION, < 60000 ) testFormat(); + testLLONGMIN(); #endif } }; From bcd76d85c3f374e5bf79d10162c509fe775fbacc Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:08 -0500 Subject: [PATCH 06/21] fix: ci_less::operator() return type --- include/boost/url/grammar/ci_string.hpp | 2 +- test/unit/grammar/ci_string.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/include/boost/url/grammar/ci_string.hpp b/include/boost/url/grammar/ci_string.hpp index 1a48d186e..8ff996d7e 100644 --- a/include/boost/url/grammar/ci_string.hpp +++ b/include/boost/url/grammar/ci_string.hpp @@ -330,7 +330,7 @@ struct ci_less { using is_transparent = void; - std::size_t + bool operator()( core::string_view s0, core::string_view s1) const noexcept diff --git a/test/unit/grammar/ci_string.cpp b/test/unit/grammar/ci_string.cpp index 953a4286e..a1143685f 100644 --- a/test/unit/grammar/ci_string.cpp +++ b/test/unit/grammar/ci_string.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include namespace boost { @@ -103,6 +104,12 @@ class ascii_test BOOST_TEST(ci_less{}("a", "aa")); BOOST_TEST(! ci_less{}("xy", "z")); + + // ci_less::operator() must return bool, + // not std::size_t + static_assert(std::is_same< + decltype(ci_less{}("a", "b")), + bool>::value, ""); } void From fc51e83eabd2c33778f6b72145e70175b28fbc97 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:08 -0500 Subject: [PATCH 07/21] fix: incorrect noexcept in segments_base::front() and back() --- include/boost/url/impl/segments_base.hpp | 4 ++-- include/boost/url/segments_base.hpp | 4 ++-- test/unit/segments_base.cpp | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/include/boost/url/impl/segments_base.hpp b/include/boost/url/impl/segments_base.hpp index 2fa76a17b..101cba6af 100644 --- a/include/boost/url/impl/segments_base.hpp +++ b/include/boost/url/impl/segments_base.hpp @@ -180,7 +180,7 @@ size() const noexcept inline std::string segments_base:: -front() const noexcept +front() const { BOOST_ASSERT(! empty()); return *begin(); @@ -189,7 +189,7 @@ front() const noexcept inline std::string segments_base:: -back() const noexcept +back() const { BOOST_ASSERT(! empty()); return *--end(); diff --git a/include/boost/url/segments_base.hpp b/include/boost/url/segments_base.hpp index 76b9404e7..1a3dd2026 100644 --- a/include/boost/url/segments_base.hpp +++ b/include/boost/url/segments_base.hpp @@ -244,7 +244,7 @@ class BOOST_SYMBOL_VISIBLE segments_base @return The first segment. */ std::string - front() const noexcept; + front() const; /** Return the last segment @@ -277,7 +277,7 @@ class BOOST_SYMBOL_VISIBLE segments_base @return The last segment. */ std::string - back() const noexcept; + back() const; /** Return an iterator to the beginning diff --git a/test/unit/segments_base.cpp b/test/unit/segments_base.cpp index 4b2989d9b..67094bfe1 100644 --- a/test/unit/segments_base.cpp +++ b/test/unit/segments_base.cpp @@ -273,11 +273,30 @@ struct segments_base_test } } + void + testFrontBackNonEmpty() + { + // front()/back() on single-element segments + // (noexcept removal regression) + { + auto rv = parse_uri_reference("/only"); + BOOST_TEST(rv.has_value()); + if(rv.has_value()) + { + segments_base const& ps( + segments_view(rv->encoded_segments())); + BOOST_TEST_EQ(ps.front(), "only"); + BOOST_TEST_EQ(ps.back(), "only"); + } + } + } + void run() { testObservers(); testRange(); + testFrontBackNonEmpty(); testJavadoc(); } }; From f4f723ee0c10ef542ea8f3844a34db54447d83c5 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:13 -0500 Subject: [PATCH 08/21] fix: recycled_ptr::get() nullptr when empty --- include/boost/url/grammar/recycled.hpp | 2 +- test/unit/grammar/recycled.cpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/include/boost/url/grammar/recycled.hpp b/include/boost/url/grammar/recycled.hpp index 251a93862..d3ac0394d 100644 --- a/include/boost/url/grammar/recycled.hpp +++ b/include/boost/url/grammar/recycled.hpp @@ -451,7 +451,7 @@ class recycled_ptr */ T* get() const noexcept { - return &p_->t; + return p_ ? &p_->t : nullptr; } /** Return the referenced object diff --git a/test/unit/grammar/recycled.cpp b/test/unit/grammar/recycled.cpp index e3a142afe..95e73c0ef 100644 --- a/test/unit/grammar/recycled.cpp +++ b/test/unit/grammar/recycled.cpp @@ -48,6 +48,14 @@ struct recycled_test BOOST_TEST(sp2->capacity() >= 1000); } + // get() returns nullptr after release + { + recycled_ptr sp; + sp->reserve(100); + sp.release(); + BOOST_TEST(sp.get() == nullptr); + } + // coverage { implementation_defined::recycled_add_impl(1); From 217c95468fbeebe365afea26c17443b9d26d884f Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:13 -0500 Subject: [PATCH 09/21] fix: format center-alignment padding --- src/detail/format_args.cpp | 2 +- test/unit/format.cpp | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/detail/format_args.cpp b/src/detail/format_args.cpp index e1659ce02..1c62d1089 100644 --- a/src/detail/format_args.cpp +++ b/src/detail/format_args.cpp @@ -187,7 +187,7 @@ format(core::string_view str, format_context& ctx, grammar::lut_chars const& cs) lpad = pad; break; case '^': - lpad = w / 2; + lpad = pad / 2; rpad = pad - lpad; break; } diff --git a/test/unit/format.cpp b/test/unit/format.cpp index 1d16254cd..855dfd504 100644 --- a/test/unit/format.cpp +++ b/test/unit/format.cpp @@ -932,6 +932,17 @@ struct format_test } } + void + testCenterAlignPad() + { + // center-alignment: lpad must not exceed + // total padding (heap overflow regression) + { + url u = urls::format("{:.^6s}", "abcd"); + BOOST_TEST_CSTR_EQ(u.buffer(), ".abcd."); + } + } + void run() { @@ -941,6 +952,7 @@ struct format_test #if !BOOST_WORKAROUND( BOOST_GCC_VERSION, < 60000 ) testFormat(); testLLONGMIN(); + testCenterAlignPad(); #endif } }; From f1d59ab397c76e056317b59f85e705964793fc5b Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:13 -0500 Subject: [PATCH 10/21] fix: decode_view::ends_with with empty string --- src/decode_view.cpp | 2 ++ test/unit/decode_view.cpp | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/decode_view.cpp b/src/decode_view.cpp index 874fb74f6..b24cca8fc 100644 --- a/src/decode_view.cpp +++ b/src/decode_view.cpp @@ -105,6 +105,8 @@ bool decode_view:: ends_with( core::string_view s ) const noexcept { + if (s.empty()) + return true; if (s.size() > size()) return false; auto it0 = end(); diff --git a/test/unit/decode_view.cpp b/test/unit/decode_view.cpp index 892607c6a..d5ec6f0f8 100644 --- a/test/unit/decode_view.cpp +++ b/test/unit/decode_view.cpp @@ -230,6 +230,12 @@ struct decode_view_test BOOST_TEST_NOT(s.ends_with("url test")); } + // ends_with() empty string regression + { + BOOST_TEST(decode_view("anything").ends_with("")); + BOOST_TEST(decode_view("").ends_with("")); + } + // find() { decode_view s(str); From 1c6e27d73e3b916be5895583242a5d255659b63b Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:13 -0500 Subject: [PATCH 11/21] fix: stale pattern n.path after colon-encoding --- src/detail/pattern.cpp | 1 + test/unit/format.cpp | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/detail/pattern.cpp b/src/detail/pattern.cpp index 6d6dea70b..a3e67971a 100644 --- a/src/detail/pattern.cpp +++ b/src/detail/pattern.cpp @@ -288,6 +288,7 @@ apply( dest0++; } } + n.path += diff; } } // 2) url has no authority and path diff --git a/test/unit/format.cpp b/test/unit/format.cpp index 855dfd504..a0e60a567 100644 --- a/test/unit/format.cpp +++ b/test/unit/format.cpp @@ -943,6 +943,17 @@ struct format_test } } + void + testColonInFirstSegment() + { + // first segment with colon triggers encoding; + // n.path must be updated after colon-encoding + { + url u = urls::format("{}", "a:b"); + BOOST_TEST_CSTR_EQ(u.encoded_path(), "a%3Ab"); + } + } + void run() { @@ -953,6 +964,7 @@ struct format_test testFormat(); testLLONGMIN(); testCenterAlignPad(); + testColonInFirstSegment(); #endif } }; From ce4755d574de832d519008fa106e3ae48374b226 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:31:13 -0500 Subject: [PATCH 12/21] fix: ci_is_less OOB read --- src/grammar/ci_string.cpp | 7 ++++--- test/unit/grammar/ci_string.cpp | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/grammar/ci_string.cpp b/src/grammar/ci_string.cpp index 199edce57..a3d5c8238 100644 --- a/src/grammar/ci_string.cpp +++ b/src/grammar/ci_string.cpp @@ -62,15 +62,16 @@ ci_is_less( { auto p1 = s0.data(); auto p2 = s1.data(); - for(auto n = s0.size();n--;) + auto n = s0.size() < s1.size() + ? s0.size() : s1.size(); + while(n--) { auto c1 = to_lower(*p1++); auto c2 = to_lower(*p2++); if(c1 != c2) return c1 < c2; } - // equal - return false; + return s0.size() < s1.size(); } } // detail diff --git a/test/unit/grammar/ci_string.cpp b/test/unit/grammar/ci_string.cpp index a1143685f..4ff5afb80 100644 --- a/test/unit/grammar/ci_string.cpp +++ b/test/unit/grammar/ci_string.cpp @@ -110,6 +110,12 @@ class ascii_test static_assert(std::is_same< decltype(ci_less{}("a", "b")), bool>::value, ""); + + // ci_is_less with mismatched lengths + // (OOB read regression) + BOOST_TEST(ci_is_less("ab", "abc")); + BOOST_TEST(! ci_is_less("abc", "ab")); + BOOST_TEST(! ci_is_less("ABC", "ab")); } void From 87fb9cf726a9698d9deae107aea4d621e766bba9 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 22:07:32 -0500 Subject: [PATCH 13/21] fix: recycled_ptr copy self-assignment --- include/boost/url/grammar/impl/recycled.hpp | 2 ++ test/unit/grammar/recycled.cpp | 37 +++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/include/boost/url/grammar/impl/recycled.hpp b/include/boost/url/grammar/impl/recycled.hpp index e57eb9540..f61839494 100644 --- a/include/boost/url/grammar/impl/recycled.hpp +++ b/include/boost/url/grammar/impl/recycled.hpp @@ -182,6 +182,8 @@ operator=( recycled_ptr const& other) noexcept -> recycled_ptr& { + if(this == &other) + return *this; BOOST_ASSERT( bin_ == other.bin_); if(p_) diff --git a/test/unit/grammar/recycled.cpp b/test/unit/grammar/recycled.cpp index 95e73c0ef..9e729e0cd 100644 --- a/test/unit/grammar/recycled.cpp +++ b/test/unit/grammar/recycled.cpp @@ -56,6 +56,43 @@ struct recycled_test BOOST_TEST(sp.get() == nullptr); } +#if defined(__clang__) && defined(__has_warning) +# if __has_warning("-Wself-assign-overloaded") || __has_warning("-Wself-move") +# pragma clang diagnostic push +# if __has_warning("-Wself-assign-overloaded") +# pragma clang diagnostic ignored "-Wself-assign-overloaded" +# endif +# if __has_warning("-Wself-move") +# pragma clang diagnostic ignored "-Wself-move" +# endif +# endif +#elif defined(__GNUC__) && !defined(__clang__) +# if __GNUC__ >= 13 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wself-move" +# endif +#endif + + // self-assignment preserves state + { + recycled_ptr sp; + sp->reserve(100); + auto cap = sp->capacity(); + sp = sp; + BOOST_TEST(sp.get() != nullptr); + BOOST_TEST_EQ(sp->capacity(), cap); + } + +#if defined(__clang__) && defined(__has_warning) +# if __has_warning("-Wself-assign-overloaded") || __has_warning("-Wself-move") +# pragma clang diagnostic pop +# endif +#elif defined(__GNUC__) && !defined(__clang__) +# if __GNUC__ >= 13 +# pragma GCC diagnostic pop +# endif +#endif + // coverage { implementation_defined::recycled_add_impl(1); From 080a11a0f44f454aa3ffd57f779a30c7816051cb Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:33:10 -0500 Subject: [PATCH 14/21] fix: url move self-assignment --- include/boost/url/impl/url.hpp | 2 ++ test/unit/url.cpp | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/include/boost/url/impl/url.hpp b/include/boost/url/impl/url.hpp index 3b0679ff4..8e6d3ffd7 100644 --- a/include/boost/url/impl/url.hpp +++ b/include/boost/url/impl/url.hpp @@ -62,6 +62,8 @@ url& url:: operator=(url&& u) noexcept { + if(this == &u) + return *this; if(s_) deallocate(s_); impl_ = u.impl_; diff --git a/test/unit/url.cpp b/test/unit/url.cpp index 2be16ae01..15ab62d6b 100644 --- a/test/unit/url.cpp +++ b/test/unit/url.cpp @@ -144,6 +144,14 @@ struct url_test BOOST_TEST_EQ(u2.buffer(), "x://y/z?q#f"); } + // self-move assignment preserves state + { + url u("http://example.com"); + auto& ref = u; + u = std::move(ref); + BOOST_TEST_EQ(u.buffer(), "http://example.com"); + } + // url(core::string_view) { url u("http://example.com/path/to/file.txt?#"); From b4ec9f4d704b71657fc29b680dad87e5612cf894 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:33:13 -0500 Subject: [PATCH 15/21] fix: encode_one signed char right-shift --- include/boost/url/detail/impl/format_args.hpp | 5 +++-- test/unit/format.cpp | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/boost/url/detail/impl/format_args.hpp b/include/boost/url/detail/impl/format_args.hpp index 759a86520..bfb46ce67 100644 --- a/include/boost/url/detail/impl/format_args.hpp +++ b/include/boost/url/detail/impl/format_args.hpp @@ -212,8 +212,9 @@ encode_one( return; } *out++ = '%'; - *out++ = urls::detail::hexdigs[0][c>>4]; - *out++ = urls::detail::hexdigs[0][c&0xf]; + auto uc = static_cast(c); + *out++ = urls::detail::hexdigs[0][uc>>4]; + *out++ = urls::detail::hexdigs[0][uc&0xf]; } // get an unsigned value from format_args diff --git a/test/unit/format.cpp b/test/unit/format.cpp index a0e60a567..feef0e87c 100644 --- a/test/unit/format.cpp +++ b/test/unit/format.cpp @@ -954,6 +954,19 @@ struct format_test } } + void + testHighByteEncode() + { + // signed char shift UB regression: encoding + // a byte with the high bit set (e.g. 0x80) + { + url u = urls::format("/{}/", + std::string(1, '\x80')); + auto p = u.encoded_path(); + BOOST_TEST(p.find("%80") != core::string_view::npos); + } + } + void run() { @@ -965,6 +978,7 @@ struct format_test testLLONGMIN(); testCenterAlignPad(); testColonInFirstSegment(); + testHighByteEncode(); #endif } }; From 8a6fd2fc5f82a1f12f06cf13fc53dfdad2f3b2d0 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:33:16 -0500 Subject: [PATCH 16/21] fix: encode() noexcept on throwing template --- include/boost/url/encode.hpp | 2 +- include/boost/url/impl/encode.hpp | 2 +- test/unit/encode.cpp | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/boost/url/encode.hpp b/include/boost/url/encode.hpp index 811e9a41f..d92f53666 100644 --- a/include/boost/url/encode.hpp +++ b/include/boost/url/encode.hpp @@ -185,7 +185,7 @@ encode( core::string_view s, CS const& allowed, encoding_opts opt = {}, - StringToken&& token = {}) noexcept; + StringToken&& token = {}); } // urls } // boost diff --git a/include/boost/url/impl/encode.hpp b/include/boost/url/impl/encode.hpp index 7bcc799d7..5645243d7 100644 --- a/include/boost/url/impl/encode.hpp +++ b/include/boost/url/impl/encode.hpp @@ -278,7 +278,7 @@ encode( core::string_view s, CS const& allowed, encoding_opts opt, - StringToken&& token) noexcept + StringToken&& token) { BOOST_CORE_STATIC_ASSERT( grammar::is_charset::value); diff --git a/test/unit/encode.cpp b/test/unit/encode.cpp index b4954064d..6ed049508 100644 --- a/test/unit/encode.cpp +++ b/test/unit/encode.cpp @@ -209,12 +209,27 @@ class encode_test } } + void + testEncodeNoexcept() + { + // encode() buffer overload must not + // be noexcept (noexcept removal regression) + { + char buf[4]; + static_assert( + !noexcept(encode( + buf, sizeof(buf), "x", pchars)), + ""); + } + } + void run() { testEncode(); testEncodeExtras(); testEncodeZeroDest(); + testEncodeNoexcept(); testJavadocs(); } }; From 0b2bf1a7ec5b0e08c03de04c812a2aa706ee0e06 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:33:19 -0500 Subject: [PATCH 17/21] fix: port_rule has_number for port zero at end of input --- .../boost/url/rfc/detail/impl/port_rule.hpp | 2 +- test/unit/rfc/authority_rule.cpp | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/include/boost/url/rfc/detail/impl/port_rule.hpp b/include/boost/url/rfc/detail/impl/port_rule.hpp index cfa802a28..a1300c8d4 100644 --- a/include/boost/url/rfc/detail/impl/port_rule.hpp +++ b/include/boost/url/rfc/detail/impl/port_rule.hpp @@ -68,7 +68,7 @@ parse( } // no digits t.str = core::string_view(start, it); - t.has_number = it != end; + t.has_number = it != start; t.number = 0; return t; } diff --git a/test/unit/rfc/authority_rule.cpp b/test/unit/rfc/authority_rule.cpp index 5e758010b..794f6be13 100644 --- a/test/unit/rfc/authority_rule.cpp +++ b/test/unit/rfc/authority_rule.cpp @@ -64,6 +64,27 @@ class authority_rule_test a.encoded_password(), "y"); } } + + // port zero at end of input + // (has_number regression) + { + auto rv = grammar::parse( + "host:0", authority_rule); + if(BOOST_TEST(rv.has_value())) + { + BOOST_TEST(rv->has_port()); + BOOST_TEST_EQ(rv->port_number(), 0); + } + } + { + auto rv = grammar::parse( + "host:000", authority_rule); + if(BOOST_TEST(rv.has_value())) + { + BOOST_TEST(rv->has_port()); + BOOST_TEST_EQ(rv->port_number(), 0); + } + } } }; From 1f2ccb4ccf82f9adf4d4a3f590c79431e69fc591 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:33:22 -0500 Subject: [PATCH 18/21] fix: ci_equal arguments by const reference --- include/boost/url/grammar/ci_string.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/boost/url/grammar/ci_string.hpp b/include/boost/url/grammar/ci_string.hpp index 8ff996d7e..24d42157d 100644 --- a/include/boost/url/grammar/ci_string.hpp +++ b/include/boost/url/grammar/ci_string.hpp @@ -295,8 +295,8 @@ struct ci_equal class String0, class String1> bool operator()( - String0 s0, - String1 s1) const noexcept + String0 const& s0, + String1 const& s1) const noexcept { return ci_is_equal(s0, s1); } From f87da97591693e947fb1760ef00a1140b3579cda Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:33:25 -0500 Subject: [PATCH 19/21] fix: decode() noexcept on throwing template --- include/boost/url/decode.hpp | 2 +- include/boost/url/impl/decode.hpp | 2 +- test/unit/decode.cpp | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/include/boost/url/decode.hpp b/include/boost/url/decode.hpp index 19e19d991..0b7ad0389 100644 --- a/include/boost/url/decode.hpp +++ b/include/boost/url/decode.hpp @@ -148,7 +148,7 @@ system::result decode( core::string_view s, encoding_opts opt = {}, - StringToken&& token = {}) noexcept; + StringToken&& token = {}); } // urls } // boost diff --git a/include/boost/url/impl/decode.hpp b/include/boost/url/impl/decode.hpp index a24a60387..d7967c30b 100644 --- a/include/boost/url/impl/decode.hpp +++ b/include/boost/url/impl/decode.hpp @@ -53,7 +53,7 @@ system::result decode( core::string_view s, encoding_opts opt, - StringToken&& token) noexcept + StringToken&& token) { static_assert( string_token::is_token< diff --git a/test/unit/decode.cpp b/test/unit/decode.cpp index 45e3ee9eb..e441391b0 100644 --- a/test/unit/decode.cpp +++ b/test/unit/decode.cpp @@ -176,12 +176,25 @@ class decode_test } } + void + testDecodeNoexcept() + { + // decode() token overload must not be + // noexcept (noexcept removal regression) + { + static_assert( + !noexcept(decode("x")), + ""); + } + } + void run() { testDecodedSize(); testDecodeBuffer(); testDecodeTokens(); + testDecodeNoexcept(); testDocExamples(); } }; From 61a597deb572bceaca6d4f3984a4eb0fc03fe760 Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Fri, 27 Feb 2026 18:33:28 -0500 Subject: [PATCH 20/21] docs: error_types copy-paste deprecation messages --- include/boost/url/error_types.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/boost/url/error_types.hpp b/include/boost/url/error_types.hpp index ae288a7b9..a9920e618 100644 --- a/include/boost/url/error_types.hpp +++ b/include/boost/url/error_types.hpp @@ -114,7 +114,7 @@ using system_error @warning This alias is no longer supported and should not be used in new code. Please use - `core::string_view` instead. + `system::generic_category` instead. This alias is included for backwards compatibility with earlier versions of the @@ -134,7 +134,7 @@ using boost::system::generic_category; @warning This alias is no longer supported and should not be used in new code. Please use - `core::string_view` instead. + `system::system_category` instead. This alias is included for backwards compatibility with earlier versions of the @@ -154,7 +154,7 @@ using boost::system::system_category; @warning This alias is no longer supported and should not be used in new code. Please use - `core::string_view` instead. + `system::errc` instead. This alias is included for backwards compatibility with earlier versions of the From 2b81110bd37702863a737ae052fc0e52df66fe6f Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Mon, 2 Mar 2026 17:13:27 -0500 Subject: [PATCH 21/21] fix: remove broken coveralls upload from Drone CI The coveralls section fails on Ubuntu 24.04 because pip3 install requires a virtual environment (PEP 668), and the resulting cpp-coveralls command-not-found exit code was failing the build. --- .drone/drone.sh | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.drone/drone.sh b/.drone/drone.sh index c9186524d..71e3fcb45 100755 --- a/.drone/drone.sh +++ b/.drone/drone.sh @@ -205,21 +205,6 @@ elif [ "$DRONE_JOB_BUILDTYPE" == "codecov" ]; then cd "$BOOST_ROOT/libs/$SELF" ci/travis/codecov.sh - # coveralls - # uses multiple lcov steps from boost-ci codecov.sh script - if [ -n "${COVERALLS_REPO_TOKEN}" ]; then - echo "processing coveralls" - pip3 install --user cpp-coveralls - cd "$BOOST_CI_SRC_FOLDER" - - export PATH=/tmp/lcov/bin:$PATH - command -v lcov - lcov --version - - lcov --remove coverage.info -o coverage_filtered.info '*/test/*' '*/extra/*' - cpp-coveralls --verbose -l coverage_filtered.info - fi - elif [ "$DRONE_JOB_BUILDTYPE" == "valgrind" ]; then echo '==================================> INSTALL'