From 0b7fd495a49d893d545509388f7338e95e84e66d Mon Sep 17 00:00:00 2001 From: Alan de Freitas Date: Tue, 21 Oct 2025 19:06:35 -0500 Subject: [PATCH] feat: segments_view iterator constructors --- include/boost/url/detail/segments_range.hpp | 163 ++++++++++++ include/boost/url/impl/segments_base.hpp | 4 + .../boost/url/impl/segments_encoded_base.hpp | 4 + include/boost/url/segments_encoded_view.hpp | 47 ++++ include/boost/url/segments_view.hpp | 47 ++++ src/segments_encoded_view.cpp | 7 + src/segments_view.cpp | 7 + test/unit/segments_encoded_view.cpp | 17 ++ test/unit/segments_view.cpp | 235 ++++++++++++++++++ 9 files changed, 531 insertions(+) create mode 100644 include/boost/url/detail/segments_range.hpp diff --git a/include/boost/url/detail/segments_range.hpp b/include/boost/url/detail/segments_range.hpp new file mode 100644 index 00000000..4bd8c11e --- /dev/null +++ b/include/boost/url/detail/segments_range.hpp @@ -0,0 +1,163 @@ +// +// Copyright (c) 2025 Alan de Freitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/url +// + +#ifndef BOOST_URL_DETAIL_SEGMENTS_RANGE_HPP +#define BOOST_URL_DETAIL_SEGMENTS_RANGE_HPP + +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace urls { +namespace detail { + +struct segments_iter_access +{ + static + segments_iter_impl const& + impl(segments_base::iterator const& it) noexcept + { + return it.it_; + } + + static + segments_iter_impl const& + impl(segments_encoded_base::iterator const& it) noexcept + { + return it.it_; + } +}; + +inline +path_ref +make_subref_from_impls( + segments_iter_impl const& first, + segments_iter_impl const& last) noexcept +{ + BOOST_ASSERT(first.ref.alias_of(last.ref)); + path_ref const& ref = first.ref; + + std::size_t const i0 = first.index; + std::size_t const i1 = last.index; + BOOST_ASSERT(i0 <= i1); + std::size_t const nseg = i1 - i0; + + bool const absolute = ref.buffer().starts_with('/'); + + // Empty range + if (nseg == 0) + { + std::size_t off0; + if (i0 == 0) + { + // [begin, begin): don't include the leading '/' + // for absolute, start right after the leading '/'; + if (absolute) + { + off0 = 1; + } + // for relative, start at the first segment character. + else + { + off0 = first.pos; + } + } + else + { + // [it, it) in the middle: + // skip the separator before segment i0 + off0 = first.pos + 1; + } + + core::string_view const sub(ref.data() + off0, 0); + return {sub, 0, 0}; + } + + // General case: non-empty range + // Start offset + std::size_t off0; + bool include_leading_slash = false; + if (i0 == 0) + { + if (absolute) + { + // include leading '/' + off0 = 0; + include_leading_slash = true; + } + else + { + // relative: start at first segment + off0 = first.pos; + } + } + else + { + // skip slash before segment i0 + off0 = first.pos + 1; + } + + // End offset + std::size_t off1; + if(i1 == ref.nseg()) + { + off1 = ref.size(); + } + else + { + // stop before the slash preceding i1 + off1 = last.pos; + } + + BOOST_ASSERT(off1 >= off0); + core::string_view const sub(ref.data() + off0, off1 - off0); + + // Decoded-length: + // sum per-segment decoded lengths + internal '/' + the leading '/'. + std::size_t dn_sum = 0; + { + // copy to iterate + segments_iter_impl cur = first; + for (std::size_t k = 0; k < nseg; ++k) + { + // per-segment decoded length + dn_sum += cur.dn; + cur.increment(); + } + // internal '/'s + dn_sum += (nseg - 1); + // leading '/' + if (include_leading_slash) + { + ++dn_sum; + } + } + + return {sub, dn_sum, nseg}; +} + +template +inline +path_ref +make_subref(Iter const& first, Iter const& last) noexcept +{ + auto const& f = segments_iter_access::impl(first); + auto const& l = segments_iter_access::impl(last); + return make_subref_from_impls(f, l); +} + +} // detail +} // urls +} // boost + +#endif diff --git a/include/boost/url/impl/segments_base.hpp b/include/boost/url/impl/segments_base.hpp index 9b10b584..91ab37bd 100644 --- a/include/boost/url/impl/segments_base.hpp +++ b/include/boost/url/impl/segments_base.hpp @@ -17,6 +17,9 @@ namespace boost { namespace urls { +namespace detail { +struct segments_iter_access; +} class segments_base::iterator { @@ -24,6 +27,7 @@ class segments_base::iterator friend class segments_base; friend class segments_ref; + friend struct detail::segments_iter_access; iterator(detail::path_ref const&) noexcept; iterator(detail::path_ref const&, int) noexcept; diff --git a/include/boost/url/impl/segments_encoded_base.hpp b/include/boost/url/impl/segments_encoded_base.hpp index 908214d9..7b57cee1 100644 --- a/include/boost/url/impl/segments_encoded_base.hpp +++ b/include/boost/url/impl/segments_encoded_base.hpp @@ -16,6 +16,9 @@ namespace boost { namespace urls { +namespace detail { +struct segments_iter_access; +} class segments_encoded_base::iterator { @@ -24,6 +27,7 @@ class segments_encoded_base::iterator friend class url_base; friend class segments_encoded_base; friend class segments_encoded_ref; + friend struct detail::segments_iter_access; iterator(detail::path_ref const&) noexcept; iterator(detail::path_ref const&, int) noexcept; diff --git a/include/boost/url/segments_encoded_view.hpp b/include/boost/url/segments_encoded_view.hpp index 63ece3df..a9dffe50 100644 --- a/include/boost/url/segments_encoded_view.hpp +++ b/include/boost/url/segments_encoded_view.hpp @@ -167,6 +167,53 @@ public: segments_encoded_view( core::string_view s); + /** Constructor + + This function creates a new @ref segments_encoded_view + from a pair of iterators referring to + elements of another encoded segments + view. The resulting view references + the same underlying character buffer + as the original. + + The caller is responsible for ensuring + that the lifetime of the original buffer + extends until the constructed view + is no longer referenced. + + @par Example + @code + segments_encoded_view ps( "/path/to/file.txt" ); + + segments_encoded_view sub( + std::next(ps.begin()), + ps.end()); + + // sub represents "to/file.txt" + @endcode + + @par Preconditions + The iterators must be valid and belong to + the same @ref segments_encoded_view. + + @par Postconditions + `sub.buffer()` references characters in the + original `ps.buffer()`. + + @par Complexity + Linear in `sub.buffer()` + + @par Exception Safety + Throws nothing. + + @param first The beginning iterator. + @param last The ending iterator. + */ + BOOST_URL_DECL + segments_encoded_view( + iterator first, + iterator last) noexcept; + /** Assignment After assignment, both views diff --git a/include/boost/url/segments_view.hpp b/include/boost/url/segments_view.hpp index 32fd5f6b..9f6135cf 100644 --- a/include/boost/url/segments_view.hpp +++ b/include/boost/url/segments_view.hpp @@ -165,6 +165,53 @@ public: segments_view( core::string_view s); + /** Constructor + + This function creates a new @ref segments_view + from a pair of iterators referring to + elements of another segments view. The + resulting view references the same + underlying character buffer as the + original. + + The caller is responsible for ensuring + that the lifetime of the original buffer + extends until the constructed view is no + longer referenced. + + @par Example + @code + segments_view ps( "/path/to/file.txt" ); + + segments_view sub( + std::next(ps.begin()), + ps.end()); + + // sub represents "to/file.txt" + @endcode + + @par Preconditions + The iterators must be valid and belong to + the same @ref segments_view. + + @par Postconditions + `sub.buffer()` references characters in the + original `ps.buffer()`. + + @par Complexity + Linear in `sub.buffer()` + + @par Exception Safety + Throws nothing. + + @param first The beginning iterator. + @param last The ending iterator. + */ + BOOST_URL_DECL + segments_view( + iterator first, + iterator last) noexcept; + /** Assignment After assignment, both views diff --git a/src/segments_encoded_view.cpp b/src/segments_encoded_view.cpp index edf5047a..2837e815 100644 --- a/src/segments_encoded_view.cpp +++ b/src/segments_encoded_view.cpp @@ -10,6 +10,7 @@ #include +#include #include #include @@ -32,6 +33,12 @@ segments_encoded_view( { } +segments_encoded_view:: +segments_encoded_view(iterator first, iterator last) noexcept + : segments_encoded_base(detail::make_subref(first, last)) +{ +} + segments_encoded_view:: operator segments_view() const noexcept diff --git a/src/segments_view.cpp b/src/segments_view.cpp index ebaa60d0..e36dd31c 100644 --- a/src/segments_view.cpp +++ b/src/segments_view.cpp @@ -10,6 +10,7 @@ #include +#include #include #include @@ -32,6 +33,12 @@ segments_view( { } +segments_view:: +segments_view(iterator first, iterator last) noexcept + : segments_base(detail::make_subref(first, last)) +{ +} + } // urls } // boost diff --git a/test/unit/segments_encoded_view.cpp b/test/unit/segments_encoded_view.cpp index 380e0b4c..a7be0e08 100644 --- a/test/unit/segments_encoded_view.cpp +++ b/test/unit/segments_encoded_view.cpp @@ -95,6 +95,23 @@ struct segments_const_encoded_view_test BOOST_TEST_THROWS(segments_encoded_view("FA%"), system::system_error); } + // segments_encoded_view(iterator, iterator) + { + segments_encoded_view ps = parse_path("/a/b/c").value(); + auto first = std::next(ps.begin()); + auto last = ps.end(); + segments_encoded_view sub(first, last); + + BOOST_TEST_EQ(sub.size(), 2u); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "b/c"); + + auto it = sub.begin(); + BOOST_TEST_EQ(*it++, "b"); + BOOST_TEST_EQ(*it++, "c"); + BOOST_TEST(it == sub.end()); + } + // operator=(segments_encoded_view) { segments_encoded_view ps0("/path/to/file.txt"); diff --git a/test/unit/segments_view.cpp b/test/unit/segments_view.cpp index 9f82b9c5..37cdefe9 100644 --- a/test/unit/segments_view.cpp +++ b/test/unit/segments_view.cpp @@ -116,6 +116,240 @@ struct segments_view_test } } + void + testRangeCtor() + { + // full slice equals original (absolute path) + { + segments_view ps = parse_path("/a/b/c").value(); + segments_view sub(ps.begin(), ps.end()); + + BOOST_TEST_EQ(sub.size(), 3u); + BOOST_TEST(sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "/a/b/c"); + // alias same storage + BOOST_TEST_EQ(sub.buffer().data(), ps.buffer().data()); + } + + // full slice equals original (relative path) + { + segments_view ps = parse_path("a/b/c").value(); + segments_view sub(ps.begin(), ps.end()); + + BOOST_TEST_EQ(sub.size(), 3u); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "a/b/c"); + // alias same storage + BOOST_TEST_EQ(sub.buffer().data(), ps.buffer().data()); + } + + // drop first segment: start at index 1 (no leading slash) + { + segments_view ps = parse_path("/a/b/c").value(); + auto first = std::next(ps.begin()); + auto last = ps.end(); + segments_view sub(first, last); + + BOOST_TEST_EQ(sub.size(), 2u); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "b/c"); + + auto it = sub.begin(); + BOOST_TEST_EQ(*it++, "b"); + BOOST_TEST_EQ(*it++, "c"); + BOOST_TEST(it == sub.end()); + } + + // take prefix without the last segment: + // [begin, prev(end)) -> "/a/b" + { + segments_view ps = parse_path("/a/b/c").value(); + auto first = ps.begin(); + auto last = std::prev(ps.end()); + segments_view sub(first, last); + + BOOST_TEST_EQ(sub.size(), 2u); + BOOST_TEST(sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "/a/b"); + + auto it = sub.begin(); + BOOST_TEST_EQ(*it++, "a"); + BOOST_TEST_EQ(*it++, "b"); + BOOST_TEST(it == sub.end()); + } + + // single segment in the middle: + // ["b", past "b") -> "b" + { + segments_view ps = parse_path("/a/b/c").value(); + auto b = std::next(ps.begin()); + auto e = std::next(b); + segments_view sub(b, e); + + BOOST_TEST_EQ(sub.size(), 1u); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "b"); + BOOST_TEST_EQ(*sub.begin(), "b"); + } + + // relative path: + // no leading slash preserved in any slice + { + segments_view ps = parse_path("a/b/c").value(); + + // full slice + segments_view sub1(ps.begin(), ps.end()); + BOOST_TEST_EQ(sub1.size(), 3u); + BOOST_TEST(!sub1.is_absolute()); + BOOST_TEST_EQ(sub1.buffer(), "a/b/c"); + + // prefix [begin, prev(end)) -> "a/b" + segments_view sub2(ps.begin(), std::prev(ps.end())); + BOOST_TEST_EQ(sub2.size(), 2u); + BOOST_TEST(!sub2.is_absolute()); + BOOST_TEST_EQ(sub2.buffer(), "a/b"); + + // middle one ["b", past "b") -> "b" + auto b = std::next(ps.begin()); + segments_view sub3(b, std::next(b)); + BOOST_TEST_EQ(sub3.size(), 1u); + BOOST_TEST(!sub3.is_absolute()); + BOOST_TEST_EQ(sub3.buffer(), "b"); + } + + // empty subrange [it, it): + // empty buffer, not absolute + { + segments_view ps = parse_path("/a/b").value(); + auto it = ps.begin(); + segments_view sub(it, it); + + BOOST_TEST_EQ(sub.size(), 0u); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), ""); + BOOST_TEST(sub.begin() == sub.end()); + } + + // empty subrange from relative source + { + segments_view ps = parse_path("a/b").value(); + auto it = ps.begin(); + segments_view sub(it, it); + + BOOST_TEST_EQ(sub.size(), 0u); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), ""); + BOOST_TEST(sub.begin() == sub.end()); + } + + // percent-encoding: + // slice of first encoded segment only, absolute start + { + segments_view ps = parse_path("/a%2Fb/c").value(); + auto first = ps.begin(); + auto last = std::next(ps.begin()); + segments_view sub(first, last); + + // encoded in buffer + BOOST_TEST(sub.is_absolute()); + BOOST_TEST_EQ(sub.size(), 1u); + BOOST_TEST_EQ(sub.buffer(), "/a%2Fb"); + + // decoded on deref + auto it = sub.begin(); + BOOST_TEST_EQ(*it++, "a/b"); + BOOST_TEST(it == sub.end()); + } + + // aliasing: url_view -> segments_view -> subrange + { + url_view u("/x/y"); + segments_view ps = u.segments(); + segments_view sub(ps.begin(), ps.end()); + + BOOST_TEST_EQ(sub.buffer().data(), u.buffer().data()); + BOOST_TEST_EQ(sub.buffer(), "/x/y"); + BOOST_TEST(sub.is_absolute()); + BOOST_TEST_EQ(sub.size(), 2u); + } + + // empty prefix of absolute path: + // [begin, begin) -> empty + { + segments_view ps = parse_path("/a/b/c").value(); + segments_view sub(ps.begin(), ps.begin()); + BOOST_TEST_EQ(sub.size(), 0u); + BOOST_TEST_EQ(sub.buffer(), ""); + BOOST_TEST(!sub.is_absolute()); + } + + // empty subrange in the middle (absolute): + // ["b","b") -> "", not absolute + { + segments_view ps = parse_path("/a/b/c").value(); + auto b = std::next(ps.begin()); + segments_view sub(b, b); + + BOOST_TEST_EQ(sub.size(), 0u); + BOOST_TEST_EQ(sub.buffer(), ""); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST(sub.begin() == sub.end()); + } + + // empty subrange in the middle (relative): + // ["b","b") -> "", not absolute + { + segments_view ps = parse_path("a/b/c").value(); + auto b = std::next(ps.begin()); // "b" + segments_view sub(b, b); + + BOOST_TEST_EQ(sub.size(), 0u); + BOOST_TEST_EQ(sub.buffer(), ""); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST(sub.begin() == sub.end()); + } + + // empty subrange at end: + // [end,end) -> "", not absolute + { + segments_view ps = parse_path("/a/b/c").value(); + auto e = ps.end(); + segments_view sub(e, e); + + BOOST_TEST_EQ(sub.size(), 0u); + BOOST_TEST_EQ(sub.buffer(), ""); + BOOST_TEST(!sub.is_absolute()); + } + + // single-segment absolute path full slice: "/a" + { + segments_view ps = parse_path("/a").value(); + segments_view sub(ps.begin(), ps.end()); + + BOOST_TEST_EQ(sub.size(), 1u); + BOOST_TEST(sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "/a"); + + auto it = sub.begin(); + BOOST_TEST_EQ(*it++, "a"); + BOOST_TEST(it == sub.end()); + } + + // middle slice using last.pos path: + // ["b", prev(end)) on "/a/b/c" -> "b" + { + segments_view ps = parse_path("/a/b/c").value(); + auto b = std::next(ps.begin()); + auto last = std::prev(ps.end()); + segments_view sub(b, last); + + BOOST_TEST_EQ(sub.size(), 1u); + BOOST_TEST(!sub.is_absolute()); + BOOST_TEST_EQ(sub.buffer(), "b"); + BOOST_TEST_EQ(*sub.begin(), "b"); + } + } + void testJavadocs() { @@ -135,6 +369,7 @@ struct segments_view_test run() { testSpecialMembers(); + testRangeCtor(); testJavadocs(); } };