diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index 8af85a453..cdd731f0d 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,6 @@ #include #include #include -#include #include #include #include diff --git a/include/boost/capy/buffers/buffer_slice.hpp b/include/boost/capy/buffers/buffer_slice.hpp index 3bfe5c33d..b509dce3f 100644 --- a/include/boost/capy/buffers/buffer_slice.hpp +++ b/include/boost/capy/buffers/buffer_slice.hpp @@ -12,117 +12,96 @@ #include #include -#include +#include +#include #include #include +#include namespace boost { namespace capy { -/** Return a byte-range slice of a buffer sequence. +/** The type produced by `buffer_slice` for a sequence `BS`. - Constructs a view over a contiguous byte range of `seq`. The - slice exposes its current bytes via `data()` (a buffer sequence) - and supports incremental consumption via `remove_prefix(n)`. - - @par Return Value - An object of unspecified type satisfying the @ref Slice concept. - Bind with `auto` and operate through the concept's members. When - `seq` models @ref MutableBufferSequence, the returned object - additionally models @ref MutableSlice. + A single buffer is closed under sub-ranging, so slicing it yields a + buffer of the same kind. Any other sequence yields the generic + `detail::slice_of` borrowed view. In both cases the result is itself + a buffer sequence — `slice_type ∈ { buffer, slice_of }`. +*/ +template +using slice_type = std::conditional_t< + std::convertible_to, + buffer_type, + detail::slice_of>; - @par Lifetime - The returned slice is associated with `seq` as its underlying - buffer sequence. `seq` — and the memory referenced by its buffer - descriptors — must remain valid for as long as the slice, or - any buffer sequence obtained from its `data()`, is in use. - Passing a temporary buffer sequence to `buffer_slice` produces - a dangling slice. +/** Return a byte sub-range of a buffer sequence, as a value. - The buffer sequence returned by `data()` is independent of the - slice object: subsequent operations on the slice (mutation, - copy, move, destruction) do not invalidate an already-obtained - `data()` view. It remains valid for as long as `seq` is valid. + The result is itself a buffer sequence (`slice_type`): pass it + directly to any operation expecting a buffer sequence — there is no + `.data()` and no separate concept to bind. For a single buffer the + result is an adjusted buffer; for any other sequence it is a borrowed + `slice_of` view. - Iterators and buffer descriptors obtained through `data()` - follow the same invalidation rules as those of `seq`. + @par Lifetime + Except for the single-buffer case, the result borrows `seq`: it stores + iterators into the sequence, not a copy. `seq` must outlive the result. + The rvalue overload is deleted so a temporary cannot be sliced into a + dangling view. - @param seq The underlying buffer sequence. Must outlive the - returned slice and any `data()` view obtained from it. + @par Complexity + Single forward pass to the cut points; never sums the whole sequence. - @param offset Number of bytes to skip from the start of `seq`. - Clamped to `buffer_size(seq)`. + @param seq The sequence to slice. Must outlive the result. + @param offset Bytes skipped from the front. Clamped to the total size. + @param length Bytes exposed, starting at `offset`. Defaults to the end. - @param length Maximum number of bytes the slice will expose, - starting at `offset`. Clamped to `buffer_size(seq) - offset`. - Defaults to the maximum value of `std::size_t`, i.e. "to end". + @return A `slice_type` value modeling the same buffer-sequence + concept as `seq` (mutable if `seq` is mutable). @par Example @code - template< ReadStream Stream, MutableBufferSequence MB > - task< io_result< std::size_t > > - read_all( Stream& stream, MB buffers ) - { - auto s = buffer_slice( buffers ); - std::size_t const total_size = buffer_size( buffers ); - std::size_t total = 0; - while( total < total_size ) - { - auto [ec, n] = co_await stream.read_some( s.data() ); - s.remove_prefix( n ); - total += n; - if( ec ) - co_return {ec, total}; - } - co_return {{}, total}; - } + co_await write(sock, buffer_slice(bufs, 0, 16384)); // first 16 KB + auto rest = buffer_slice(bufs, n); // drop first n @endcode - @see Slice, MutableSlice + @see slice_type, consuming_buffers */ template requires MutableBufferSequence || ConstBufferSequence -auto +slice_type buffer_slice( BufferSequence const& seq, std::size_t offset = 0, std::size_t length = (std::numeric_limits::max)()) noexcept { - return detail::slice_impl(seq, offset, length); + if constexpr (std::convertible_to) + { + // A single buffer is its own slice: advance and (maybe) truncate. + buffer_type b = seq; + b += offset; // operator+= clamps to size() + if (length < b.size()) + b = buffer_type(b.data(), length); + return b; + } + else + { + return detail::slice_of(seq, offset, length); + } } -/** Deleted overload that rejects rvalue arguments at compile time. - - Because the returned slice's validity depends on the underlying - buffer sequence remaining alive, calling `buffer_slice` with a - temporary buffer sequence would produce an immediately dangling - slice. This overload makes such calls ill-formed, surfacing the - lifetime error at compile time rather than as runtime UB. - - To slice a buffer sequence produced as a temporary, hoist it - into a named variable first: - - @code - auto bufs = some_dynamic_buffer.data(); // named, lives in scope - auto s = buffer_slice( bufs ); // OK - @endcode - - @param seq An rvalue buffer sequence (`const&&`). Binding the - slice to a temporary would dangle, so this overload is - deleted to reject such calls at compile time. - - @param offset Number of bytes to skip from the start of `seq`. +/** Deleted rvalue overload. - @param length Maximum number of bytes the slice would expose, - starting at `offset`. + Slicing a temporary would yield an immediately dangling view (the + result borrows the sequence). Hoist the sequence into a named variable + first. */ template requires MutableBufferSequence || ConstBufferSequence -auto +slice_type buffer_slice( BufferSequence const&& seq, std::size_t offset = 0, diff --git a/include/boost/capy/buffers/consuming_buffers.hpp b/include/boost/capy/buffers/consuming_buffers.hpp new file mode 100644 index 000000000..4d3786ea7 --- /dev/null +++ b/include/boost/capy/buffers/consuming_buffers.hpp @@ -0,0 +1,120 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// 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/cppalliance/capy +// + +#ifndef BOOST_CAPY_BUFFERS_CONSUMING_BUFFERS_HPP +#define BOOST_CAPY_BUFFERS_CONSUMING_BUFFERS_HPP + +#include +#include +#include + +#include +#include + +namespace boost { +namespace capy { + +/** A cursor that drives consumption of a buffer sequence. + + `consuming_buffers` is the dedicated driver for `read_some`/`write_some` + loops: it presents the not-yet-consumed bytes of a buffer sequence via + `data()`, and `consume(n)` advances past `n` transferred bytes **in + place**. + + It is deliberately **not** itself a buffer sequence — it hands out the + remaining bytes through `data()` (returning a `slice_of` view). It + **borrows** the underlying sequence (iterators + a consumed-byte offset); + the sequence must outlive the cursor, which is the natural case when the + cursor is a local of a composed operation that took its buffers by value. + + @par Example + @code + consuming_buffers consuming(buffers); + std::size_t total = 0, want = buffer_size(buffers); + while (total < want) + { + auto [ec, n] = co_await stream.read_some(consuming.data()); + consuming.consume(n); + total += n; + if (ec && total < want) co_return {ec, total}; + } + @endcode + + @see buffer_slice, slice_of +*/ +template + requires MutableBufferSequence || ConstBufferSequence +class consuming_buffers +{ +public: + /// The buffer type of the underlying sequence. + using buffer_type = capy::buffer_type; + +private: + using iterator_type = + decltype(capy::begin(std::declval())); + + iterator_type first_{}; + iterator_type last_{}; + std::size_t front_skip_ = 0; // bytes consumed from *first_ + +public: + /** Construct a cursor over `s`. + + @param s The sequence to consume. Must outlive the cursor. + */ + explicit consuming_buffers(Seq const& s) noexcept + : first_(capy::begin(s)) + , last_(capy::end(s)) + { + } + + /// Reject construction from a temporary (the view would dangle). + consuming_buffers(Seq const&&) = delete; + + /// Return the remaining (unconsumed) bytes as a buffer sequence. + detail::slice_of + data() const noexcept + { + return detail::slice_of(first_, last_, front_skip_, 0); + } + + /** Discard `n` bytes from the front, in place. + + Advances past `min(n, remaining)` bytes. + + @param n The number of bytes consumed. + */ + void + consume(std::size_t n) noexcept + { + while (n > 0 && first_ != last_) + { + std::size_t const sz = buffer_type(*first_).size(); + std::size_t const avail = sz - front_skip_; + if (n < avail) + { + front_skip_ += n; + return; + } + n -= avail; + ++first_; + front_skip_ = 0; + } + } +}; + +// CTAD: deduce the sequence type from the constructor argument. +template +consuming_buffers(Seq const&) -> consuming_buffers; + +} // namespace capy +} // namespace boost + +#endif diff --git a/include/boost/capy/concept/slice.hpp b/include/boost/capy/concept/slice.hpp deleted file mode 100644 index 928d2e190..000000000 --- a/include/boost/capy/concept/slice.hpp +++ /dev/null @@ -1,136 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// -// 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/cppalliance/capy -// - -#ifndef BOOST_CAPY_CONCEPT_SLICE_HPP -#define BOOST_CAPY_CONCEPT_SLICE_HPP - -#include -#include - -#include - -namespace boost { -namespace capy { - -/** Concept for types that view a byte sub-range of a buffer sequence. - - A type satisfies `Slice` if it provides a view over a contiguous - byte range within an underlying buffer sequence, with an operation - to advance the start and exposure of the current bytes as a buffer - sequence. - - @par Syntactic Requirements - @li `cs.data()` returns a @ref ConstBufferSequence - @li `s.remove_prefix(n)` advances the start of the slice by `n` bytes - - @par Semantic Requirements - @li `s.data()` returns a buffer sequence view of the slice's current - live bytes. - @li `s.remove_prefix(n)` makes the first `min(n, total_live_bytes)` - bytes no longer part of the slice. - - @par Lifetime - A `Slice` is associated, on construction, with an underlying - buffer sequence. The slice is valid for as long as that sequence - — and the memory referenced by its buffer descriptors — remains - valid. Operating on a slice whose underlying sequence is no - longer valid is undefined behavior. - - The buffer sequence returned by `data()` is independent of the - slice object: subsequent operations on the slice - (`remove_prefix`, copy, move, destruction) do not invalidate - an already-obtained `data()` view. It remains valid for as - long as the slice's underlying buffer sequence is valid. - - Buffer descriptors obtained through `data()` follow the same - invalidation rules as those of the underlying sequence. - - @par Concrete Types - Objects modeling `Slice` are produced by the @ref buffer_slice free - function. The concrete type returned by `buffer_slice` is unspecified; - user code should bind it with `auto` and rely on this concept. When - the underlying buffer sequence models @ref MutableBufferSequence, the - returned object additionally models @ref MutableSlice. - - @par Example - @code - template< WriteStream Stream, Slice S > - task<> write_all( Stream& stream, S s, std::size_t total ) - { - std::size_t sent = 0; - while( sent < total ) - { - auto [ec, n] = co_await stream.write_some( s.data() ); - s.remove_prefix( n ); - sent += n; - if( ec ) - co_return; - } - } - @endcode - - @see buffer_slice, MutableSlice, ConstBufferSequence -*/ -template -concept Slice = - requires(T& s, T const& cs, std::size_t n) - { - { cs.data() } -> ConstBufferSequence; - s.remove_prefix(n); - }; - -/** Concept for slices whose `data()` exposes writable buffers. - - A type satisfies `MutableSlice` if it satisfies @ref Slice and - its `data()` member additionally returns a - @ref MutableBufferSequence. This is the slice analog of the - @ref MutableBufferSequence refinement of @ref ConstBufferSequence. - - Use `MutableSlice` to constrain generic code that needs to pass - the slice's current bytes to a @ref ReadStream's `read_some` or - any other operation requiring write access through the buffer - sequence. - - @par Producing a MutableSlice - @ref buffer_slice returns an object modeling `MutableSlice` when - the input buffer sequence models @ref MutableBufferSequence. When - the input is only @ref ConstBufferSequence, the returned object - models @ref Slice but not `MutableSlice`. - - @par Example - @code - template< ReadStream Stream, MutableSlice S > - task<> read_all( Stream& stream, S s, std::size_t total ) - { - std::size_t received = 0; - while( received < total ) - { - auto [ec, n] = co_await stream.read_some( s.data() ); - s.remove_prefix( n ); - received += n; - if( ec ) - co_return; - } - } - @endcode - - @see Slice, buffer_slice, MutableBufferSequence -*/ -template -concept MutableSlice = - Slice && - requires(T const& cs) - { - { cs.data() } -> MutableBufferSequence; - }; - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/detail/slice_impl.hpp b/include/boost/capy/detail/slice_impl.hpp deleted file mode 100644 index ec05cc5b7..000000000 --- a/include/boost/capy/detail/slice_impl.hpp +++ /dev/null @@ -1,305 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// -// 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/cppalliance/capy -// - -/* - Implementation type for the public buffer_slice() free function. - Users see this only via auto + the Slice concept; the type is - documented as unspecified. Maintained alongside Slice in - include/boost/capy/concept/slice.hpp. -*/ - -#ifndef BOOST_CAPY_DETAIL_SLICE_IMPL_HPP -#define BOOST_CAPY_DETAIL_SLICE_IMPL_HPP - -#include -#include - -#include -#include -#include - -namespace boost { -namespace capy { -namespace detail { - -template -struct slice_buffer_type_for; - -template -struct slice_buffer_type_for -{ - using type = mutable_buffer; -}; - -template - requires (!MutableBufferSequence) -struct slice_buffer_type_for -{ - using type = const_buffer; -}; - -template - requires MutableBufferSequence - || ConstBufferSequence -class slice_impl -{ -public: - using iterator_type = - decltype(capy::begin(std::declval())); - using end_iterator_type = - decltype(capy::end(std::declval())); - using buffer_type = - typename slice_buffer_type_for::type; - -private: - iterator_type first_{}; - end_iterator_type last_{}; - std::size_t front_skip_ = 0; - std::size_t back_skip_ = 0; - - static buffer_type adjust_buffer( - buffer_type const& buf, - std::size_t front_n, - std::size_t back_n) noexcept - { - if constexpr (std::is_same_v) - { - return mutable_buffer( - static_cast(buf.data()) + front_n, - buf.size() - front_n - back_n); - } - else - { - return const_buffer( - static_cast(buf.data()) + front_n, - buf.size() - front_n - back_n); - } - } - -public: - /// View returned by `slice_impl::data()`. - class data_view - { - iterator_type first_{}; - end_iterator_type last_{}; - std::size_t front_skip_ = 0; - std::size_t back_skip_ = 0; - - public: - class const_iterator - { - iterator_type cur_{}; - iterator_type anchor_first_{}; - end_iterator_type anchor_last_{}; - std::size_t front_skip_ = 0; - std::size_t back_skip_ = 0; - - public: - using iterator_category = std::bidirectional_iterator_tag; - using value_type = buffer_type; - using difference_type = std::ptrdiff_t; - using pointer = value_type*; - using reference = value_type; - - const_iterator() noexcept = default; - - const_iterator( - iterator_type cur, - iterator_type anchor_first, - end_iterator_type anchor_last, - std::size_t front_skip, - std::size_t back_skip) noexcept - : cur_(cur) - , anchor_first_(anchor_first) - , anchor_last_(anchor_last) - , front_skip_(front_skip) - , back_skip_(back_skip) - { - } - - bool operator==(const_iterator const& other) const noexcept - { - return cur_ == other.cur_; - } - - bool operator!=(const_iterator const& other) const noexcept - { - return !(*this == other); - } - - value_type operator*() const noexcept - { - buffer_type buf = *cur_; - auto front_n = (cur_ == anchor_first_) ? front_skip_ : 0; - auto next = cur_; - ++next; - auto back_n = (next == anchor_last_) ? back_skip_ : 0; - return adjust_buffer(buf, front_n, back_n); - } - - const_iterator& operator++() noexcept - { - ++cur_; - return *this; - } - - const_iterator operator++(int) noexcept - { - const_iterator tmp = *this; - ++*this; - return tmp; - } - - const_iterator& operator--() noexcept - { - --cur_; - return *this; - } - - const_iterator operator--(int) noexcept - { - const_iterator tmp = *this; - --*this; - return tmp; - } - }; - - data_view() noexcept = default; - - data_view( - iterator_type first, - end_iterator_type last, - std::size_t front_skip, - std::size_t back_skip) noexcept - : first_(first) - , last_(last) - , front_skip_(front_skip) - , back_skip_(back_skip) - { - } - - const_iterator begin() const noexcept - { - return const_iterator( - first_, first_, last_, front_skip_, back_skip_); - } - - const_iterator end() const noexcept - { - return const_iterator( - last_, first_, last_, front_skip_, back_skip_); - } - }; - - slice_impl() noexcept = default; // LCOV_EXCL_LINE defaulted ctor, gcov counts a phantom line - - explicit slice_impl(BufferSequence const& bs) noexcept - : first_(capy::begin(bs)) - , last_(capy::end(bs)) - { - } - - slice_impl( - BufferSequence const& bs, - std::size_t offset, - std::size_t length) noexcept - { - auto it_begin = capy::begin(bs); - auto it_end = capy::end(bs); - - std::size_t total = 0; - for (auto it = it_begin; it != it_end; ++it) - total += (*it).size(); - - if (offset > total) - offset = total; - std::size_t const remaining = total - offset; - if (length > remaining) - length = remaining; - - first_ = it_begin; - last_ = it_end; - - std::size_t skip = offset; - while (first_ != last_) - { - std::size_t const buf_size = (*first_).size(); - if (skip < buf_size) - { - front_skip_ = skip; - break; - } - skip -= buf_size; - ++first_; - } - - std::size_t left = length; - auto cursor = first_; - std::size_t cursor_front = front_skip_; - while (cursor != last_ && left > 0) - { - std::size_t const buf_size = (*cursor).size(); - std::size_t const avail = buf_size - cursor_front; - if (left <= avail) - { - back_skip_ = avail - left; - ++cursor; - last_ = cursor; - return; - } - left -= avail; - ++cursor; - cursor_front = 0; - } - - last_ = cursor; - } - - data_view data() const noexcept - { - return data_view(first_, last_, front_skip_, back_skip_); - } - - void remove_prefix(std::size_t n) noexcept - { - while (n > 0 && first_ != last_) - { - std::size_t const buf_total = (*first_).size(); - std::size_t live = buf_total - front_skip_; - auto next = first_; - ++next; - bool const is_last = (next == last_); - if (is_last) - live -= back_skip_; - - if (n < live) - { - front_skip_ += n; - return; - } - - n -= live; - if (is_last) - { - first_ = last_; - front_skip_ = 0; - back_skip_ = 0; - return; - } - ++first_; - front_skip_ = 0; - } - } -}; - -} // namespace detail -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/detail/slice_of.hpp b/include/boost/capy/detail/slice_of.hpp new file mode 100644 index 000000000..740727dc7 --- /dev/null +++ b/include/boost/capy/detail/slice_of.hpp @@ -0,0 +1,227 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// 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/cppalliance/capy +// + +#ifndef BOOST_CAPY_DETAIL_SLICE_OF_HPP +#define BOOST_CAPY_DETAIL_SLICE_OF_HPP + +#include +#include + +#include +#include +#include +#include + +namespace boost { +namespace capy { +namespace detail { + +/** A borrowed view over a byte sub-range of a buffer sequence. + + `slice_of` is the generic result of `buffer_slice` for a sequence + that is not closed under sub-ranging (everything except a single + buffer). It models the same buffer-sequence concept as `BS` + (`MutableBufferSequence` if `BS` is mutable, otherwise + `ConstBufferSequence`), so it can be passed anywhere a buffer sequence + is expected. + + It stores iterators into the underlying sequence plus front/back byte + offsets; it neither owns nor copies the descriptors. The underlying + sequence must outlive the view. + + @par Complexity + Construction is a single forward pass to the cut points: O(buffers up + to `offset`) for a to-end slice, O(buffers up to `offset + length`) + for a bounded slice. It never sums the whole sequence. +*/ +template + requires MutableBufferSequence || ConstBufferSequence +class slice_of +{ +public: + /// The buffer type yielded by iteration. + using buffer_type = capy::buffer_type; + + /// The underlying sequence's iterator type. + using iterator_type = + decltype(capy::begin(std::declval())); + +private: + iterator_type first_{}; + iterator_type last_{}; + std::size_t front_skip_ = 0; // bytes trimmed from *first_ + std::size_t back_skip_ = 0; // bytes trimmed from the final buffer + + static buffer_type + adjust(buffer_type const& b, + std::size_t front_n, std::size_t back_n) noexcept + { + if constexpr (std::is_same_v) + return mutable_buffer( + static_cast(b.data()) + front_n, + b.size() - front_n - back_n); + else + return const_buffer( + static_cast(b.data()) + front_n, + b.size() - front_n - back_n); + } + +public: + /// Bidirectional iterator that adjusts the first and last buffers. + class const_iterator + { + iterator_type cur_{}; + iterator_type anchor_first_{}; + iterator_type anchor_last_{}; + std::size_t front_skip_ = 0; + std::size_t back_skip_ = 0; + + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = buffer_type; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type; + + const_iterator() noexcept = default; + + const_iterator( + iterator_type cur, iterator_type anchor_first, + iterator_type anchor_last, std::size_t front_skip, + std::size_t back_skip) noexcept + : cur_(cur) + , anchor_first_(anchor_first) + , anchor_last_(anchor_last) + , front_skip_(front_skip) + , back_skip_(back_skip) + { + } + + bool operator==(const_iterator const& o) const noexcept + { + return cur_ == o.cur_; + } + + bool operator!=(const_iterator const& o) const noexcept + { + return !(*this == o); + } + + value_type operator*() const noexcept + { + buffer_type buf = *cur_; + auto const front_n = (cur_ == anchor_first_) ? front_skip_ : 0; + auto next = cur_; + ++next; + auto const back_n = (next == anchor_last_) ? back_skip_ : 0; + return adjust(buf, front_n, back_n); + } + + const_iterator& operator++() noexcept { ++cur_; return *this; } + const_iterator operator++(int) noexcept + { auto t = *this; ++*this; return t; } + const_iterator& operator--() noexcept { --cur_; return *this; } + const_iterator operator--(int) noexcept + { auto t = *this; --*this; return t; } + }; + + /// Construct an empty slice. + slice_of() noexcept = default; + + /** Construct a view of `[offset, offset + length)` bytes of `bs`. + + @param bs The underlying sequence (must outlive the view). + @param offset Bytes skipped from the front. Clamped to the total. + @param length Bytes exposed; the default exposes to the end. + */ + slice_of( + BS const& bs, + std::size_t offset, + std::size_t length = + (std::numeric_limits::max)()) noexcept + { + first_ = capy::begin(bs); + last_ = capy::end(bs); + + // Position first_/front_skip_ at byte `offset` (single forward pass). + std::size_t skip = offset; + while (first_ != last_) + { + std::size_t const sz = buffer_type(*first_).size(); + if (skip < sz) + { + front_skip_ = skip; + break; + } + skip -= sz; + ++first_; + } + + if (first_ == last_) + return; // offset at or past the end: empty slice + if (length == (std::numeric_limits::max)()) + return; // to-end: last_ already end(bs), back_skip_ == 0 + + // Walk `length` live bytes forward to fix last_/back_skip_. + std::size_t left = length; + auto cursor = first_; + std::size_t cursor_front = front_skip_; + while (cursor != last_ && left > 0) + { + std::size_t const sz = buffer_type(*cursor).size(); + std::size_t const avail = sz - cursor_front; + if (left <= avail) + { + back_skip_ = avail - left; + ++cursor; + last_ = cursor; + return; + } + left -= avail; + ++cursor; + cursor_front = 0; + } + last_ = cursor; + } + + /** Construct directly from a positioned iterator range. + + Used by `consuming_buffers::data()` to expose its current position + without re-walking from the start. + */ + slice_of( + iterator_type first, iterator_type last, + std::size_t front_skip = 0, std::size_t back_skip = 0) noexcept + : first_(first) + , last_(last) + , front_skip_(front_skip) + , back_skip_(back_skip) + { + } + + /// Return an iterator to the first buffer. + const_iterator begin() const noexcept + { + return const_iterator( + first_, first_, last_, front_skip_, back_skip_); + } + + /// Return an iterator past the last buffer. + const_iterator end() const noexcept + { + return const_iterator( + last_, first_, last_, front_skip_, back_skip_); + } +}; + +} // namespace detail +} // namespace capy +} // namespace boost + +#endif diff --git a/include/boost/capy/io/write_now.hpp b/include/boost/capy/io/write_now.hpp index 2522cbbff..1c1a585e5 100644 --- a/include/boost/capy/io/write_now.hpp +++ b/include/boost/capy/io/write_now.hpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include #include @@ -334,12 +334,12 @@ class write_now { std::size_t const total_size = buffer_size(buffers); std::size_t total_written = 0; - auto cb = buffer_slice(buffers); + consuming_buffers cb(buffers); while(total_written < total_size) { auto r = co_await stream_.write_some(cb.data()); - cb.remove_prefix(std::get<0>(r.values)); + cb.consume(std::get<0>(r.values)); total_written += std::get<0>(r.values); if(r.ec) co_return io_result{ @@ -359,7 +359,7 @@ class write_now // GCC ICE in expand_expr_real_1 (expr.cc:11376) // when the buffer slice spans a co_yield, so // the GCC path uses a separate simple coroutine. - auto cb = buffer_slice(buffers); + consuming_buffers cb(buffers); while(total_written < total_size) { auto inner = stream_.write_some(cb.data()); @@ -369,7 +369,7 @@ class write_now if(r.ec) co_return io_result{ r.ec, total_written}; - cb.remove_prefix(std::get<0>(r.values)); + cb.consume(std::get<0>(r.values)); total_written += std::get<0>(r.values); } @@ -383,7 +383,7 @@ class write_now { auto r = co_await stream_.write_some(cb.data()); - cb.remove_prefix(std::get<0>(r.values)); + cb.consume(std::get<0>(r.values)); total_written += std::get<0>(r.values); if(r.ec) co_return io_result{ diff --git a/include/boost/capy/read.hpp b/include/boost/capy/read.hpp index 054771216..9cce67b45 100644 --- a/include/boost/capy/read.hpp +++ b/include/boost/capy/read.hpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -95,14 +95,14 @@ auto read(S& stream, MB buffers) -> io_task { - auto consuming = buffer_slice(buffers); + consuming_buffers consuming(buffers); std::size_t const total_size = buffer_size(buffers); std::size_t total_read = 0; while(total_read < total_size) { auto [ec, n] = co_await stream.read_some(consuming.data()); - consuming.remove_prefix(n); + consuming.consume(n); total_read += n; // A contingency that still completed the transfer is a success: // report it only when the buffer was not filled. diff --git a/include/boost/capy/test/bufgrind.hpp b/include/boost/capy/test/bufgrind.hpp index 48446ad76..c5fb89d67 100644 --- a/include/boost/capy/test/bufgrind.hpp +++ b/include/boost/capy/test/bufgrind.hpp @@ -96,12 +96,12 @@ class bufgrind std::size_t pos_ = 0; public: - /// The slice type produced for each half of a split. + /// The buffer-sequence type produced for each half of a split. using slice_type = std::decay_t< decltype(buffer_slice(std::declval()))>; - /// The type returned by @ref next. Each half is a Slice; use - /// `.data()` to obtain the buffer sequence view. + /// The type returned by @ref next. Each half is itself a buffer + /// sequence (the value returned by `buffer_slice`). using split_type = std::pair; /** Construct a buffer grinder. diff --git a/include/boost/capy/write.hpp b/include/boost/capy/write.hpp index 31a1f8208..354123ab4 100644 --- a/include/boost/capy/write.hpp +++ b/include/boost/capy/write.hpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include @@ -94,14 +94,14 @@ namespace capy { template auto write(S& stream, CB buffers) -> io_task { - auto consuming = buffer_slice(buffers); + consuming_buffers consuming(buffers); std::size_t const total_size = buffer_size(buffers); std::size_t total_written = 0; while(total_written < total_size) { auto [ec, n] = co_await stream.write_some(consuming.data()); - consuming.remove_prefix(n); + consuming.consume(n); total_written += n; // A contingency that still completed the transfer is a success: // report it only when not all bytes were written. diff --git a/test/unit/buffers/buffer_slice.cpp b/test/unit/buffers/buffer_slice.cpp index 29440a3be..ed5286cce 100644 --- a/test/unit/buffers/buffer_slice.cpp +++ b/test/unit/buffers/buffer_slice.cpp @@ -7,16 +7,14 @@ // Official repository: https://github.com/cppalliance/capy // -// Test that headers are self-contained. +// Test that the header is self-contained. #include -#include -#include #include #include +#include #include -#include #include #include "test_suite.hpp" @@ -26,405 +24,132 @@ namespace capy { namespace { -// Flatten the bytes exposed by a Slice's data() into a std::string for -// byte-exact comparison. -template -std::string flatten(Slice const& s) +// Flatten the bytes of a buffer sequence for byte-exact comparison. +template +std::string +flatten(Seq const& s) { std::string out; - auto view = s.data(); - for (auto it = view.begin(); it != view.end(); ++it) + for (auto it = capy::begin(s); it != capy::end(s); ++it) { - auto buf = *it; - out.append( - static_cast(buf.data()), - buf.size()); + const_buffer b(*it); + out.append(static_cast(b.data()), b.size()); } return out; } -} // anonymous namespace - struct buffer_slice_test { void - testConceptModeled() + testReturnsBufferSequence() { - char a[10]; - std::array mbufs = { - mutable_buffer(a, sizeof(a)) - }; - std::array cbufs = { - const_buffer(a, sizeof(a)) - }; - using m_slice = detail::slice_impl; - using c_slice = detail::slice_impl; - - // Both satisfy Slice - static_assert(Slice, - "mutable-input slice_impl must satisfy Slice"); - static_assert(Slice, - "const-input slice_impl must satisfy Slice"); - - // Only the mutable-input one satisfies MutableSlice - static_assert(MutableSlice, - "mutable-input slice_impl must satisfy MutableSlice"); - static_assert(!MutableSlice, - "const-input slice_impl must NOT satisfy MutableSlice"); - - // Default-constructed slice_impl is a valid empty slice. - { - c_slice s; - auto view = s.data(); - BOOST_TEST(view.begin() == view.end()); - } - } - - void - testNotABufferSequence() - { - char a[10]; - std::array bufs = { - mutable_buffer(a, sizeof(a)) - }; - using slice_t = detail::slice_impl; - static_assert( - !ConstBufferSequence, - "slice_impl must not model ConstBufferSequence"); - static_assert( - !MutableBufferSequence, - "slice_impl must not model MutableBufferSequence"); + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + auto s = buffer_slice(bufs, 0); + // The result IS a buffer sequence (not a Slice with .data()). + static_assert(MutableBufferSequence, + "buffer_slice's result must model MutableBufferSequence"); + static_assert(std::same_as>, + "buffer_slice must return slice_type"); + BOOST_TEST_EQ(buffer_size(s), 30u); + BOOST_TEST_EQ(flatten(s), std::string(10, 'A') + std::string(20, 'B')); } void - testDataIsBufferSequence() + testConstInput() { char a[10]; - std::array bufs = { - mutable_buffer(a, sizeof(a)) - }; - detail::slice_impl s(bufs); - using data_t = decltype(s.data()); - static_assert( - MutableBufferSequence, - "data() return must satisfy MutableBufferSequence " - "when input is mutable"); - static_assert( - ConstBufferSequence, - "data() return must satisfy ConstBufferSequence"); - static_assert( - std::ranges::bidirectional_range, - "data() return must be a bidirectional_range"); + std::array cb = { const_buffer(a, sizeof(a)) }; + auto s = buffer_slice(cb); + static_assert(ConstBufferSequence, + "buffer_slice over const input must model ConstBufferSequence"); + static_assert(!MutableBufferSequence, + "buffer_slice over const input must NOT model MutableBufferSequence"); } void - testWholeSequenceCtor() + testOffsetLength() { - char a[10]; - char b[20]; + char a[10], b[20]; std::memset(a, 'A', sizeof(a)); std::memset(b, 'B', sizeof(b)); std::array bufs = { - mutable_buffer(a, sizeof(a)), - mutable_buffer(b, sizeof(b)) - }; - detail::slice_impl s(bufs); - BOOST_TEST_EQ(buffer_size(s.data()), sizeof(a) + sizeof(b)); + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; - std::string const expected = - std::string(sizeof(a), 'A') + std::string(sizeof(b), 'B'); - BOOST_TEST_EQ(flatten(s), expected); + auto s = buffer_slice(bufs, 5, 10); + BOOST_TEST_EQ(buffer_size(s), 10u); + BOOST_TEST_EQ(flatten(s), std::string(5, 'A') + std::string(5, 'B')); } void - testOffsetLengthCtor() + testOffsetToEnd() { - char a[10]; - char b[20]; + char a[10], b[20]; std::memset(a, 'A', sizeof(a)); std::memset(b, 'B', sizeof(b)); std::array bufs = { - mutable_buffer(a, sizeof(a)), - mutable_buffer(b, sizeof(b)) - }; - using slice_t = detail::slice_impl; + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; - // offset=0, length=total -> whole sequence - { - slice_t s(bufs, 0, 30); - BOOST_TEST_EQ(buffer_size(s.data()), 30u); - BOOST_TEST_EQ(flatten(s), - std::string(10, 'A') + std::string(20, 'B')); - } - - // offset inside first buffer (front trim, no back trim) - { - slice_t s(bufs, 3, 27); - BOOST_TEST_EQ(buffer_size(s.data()), 27u); - BOOST_TEST_EQ(flatten(s), - std::string(7, 'A') + std::string(20, 'B')); - } - - // offset past first buffer, length terminating inside last (front + back) - { - slice_t s(bufs, 12, 5); - BOOST_TEST_EQ(buffer_size(s.data()), 5u); - BOOST_TEST_EQ(flatten(s), std::string(5, 'B')); - } - - // both offset and length inside first buffer - { - slice_t s(bufs, 2, 4); - BOOST_TEST_EQ(buffer_size(s.data()), 4u); - BOOST_TEST_EQ(flatten(s), std::string(4, 'A')); - } - - // offset=0, length=0 -> empty - { - slice_t s(bufs, 0, 0); - BOOST_TEST_EQ(buffer_size(s.data()), 0u); - BOOST_TEST_EQ(flatten(s), std::string()); - } - - // offset >= total -> empty (no UB) - { - slice_t s(bufs, 50, 10); - BOOST_TEST_EQ(buffer_size(s.data()), 0u); - } - - // length > total - offset -> clamped to remainder - { - slice_t s(bufs, 5, 999); - BOOST_TEST_EQ(buffer_size(s.data()), 25u); - BOOST_TEST_EQ(flatten(s), - std::string(5, 'A') + std::string(20, 'B')); - } + auto s = buffer_slice(bufs, 12); + BOOST_TEST_EQ(buffer_size(s), 18u); + BOOST_TEST_EQ(flatten(s), std::string(18, 'B')); } void - testRemovePrefix() + testSingleBufferSelfSlice() { char a[10]; - char b[20]; std::memset(a, 'A', sizeof(a)); - std::memset(b, 'B', sizeof(b)); - std::array bufs = { - mutable_buffer(a, sizeof(a)), - mutable_buffer(b, sizeof(b)) - }; - using slice_t = detail::slice_impl; - // remove within first buffer - { - slice_t s(bufs); - s.remove_prefix(3); - BOOST_TEST_EQ(buffer_size(s.data()), 27u); - BOOST_TEST_EQ(flatten(s), - std::string(7, 'A') + std::string(20, 'B')); - } - - // remove exactly to end of first buffer - { - slice_t s(bufs); - s.remove_prefix(10); - BOOST_TEST_EQ(buffer_size(s.data()), 20u); - BOOST_TEST_EQ(flatten(s), std::string(20, 'B')); - } - - // remove crossing into second buffer - { - slice_t s(bufs); - s.remove_prefix(15); - BOOST_TEST_EQ(buffer_size(s.data()), 15u); - BOOST_TEST_EQ(flatten(s), std::string(15, 'B')); - } - - // remove all - { - slice_t s(bufs); - s.remove_prefix(30); - BOOST_TEST_EQ(buffer_size(s.data()), 0u); - } - - // remove more than total -> empty, no UB - { - slice_t s(bufs); - s.remove_prefix(1000); - BOOST_TEST_EQ(buffer_size(s.data()), 0u); - } - } - - void - testRemovePrefixOnLengthCapped() - { - // Verify remove_prefix walks correctly through a slice that has - // back_skip_ set by an offset/length constructor. - char a[5]; - char b[5]; - char c[5]; - std::memset(a, 'a', sizeof(a)); - std::memset(b, 'b', sizeof(b)); - std::memset(c, 'c', sizeof(c)); - std::array bufs = { - mutable_buffer(a, sizeof(a)), - mutable_buffer(b, sizeof(b)), - mutable_buffer(c, sizeof(c)) - }; - using slice_t = detail::slice_impl; - - // bytes 2..12 -> [3 'a' + 5 'b' + 2 'c'] - slice_t s(bufs, 2, 10); - BOOST_TEST_EQ(buffer_size(s.data()), 10u); - BOOST_TEST_EQ(flatten(s), - std::string(3, 'a') + std::string(5, 'b') + std::string(2, 'c')); - - // remove 4 -> [4 'b' + 2 'c'] (consumed 3 'a' + 1 'b') - s.remove_prefix(4); - BOOST_TEST_EQ(buffer_size(s.data()), 6u); - BOOST_TEST_EQ(flatten(s), - std::string(4, 'b') + std::string(2, 'c')); - - // remove 5 -> [1 'c'] (consumed 4 'b' + 1 'c') - s.remove_prefix(5); - BOOST_TEST_EQ(buffer_size(s.data()), 1u); - BOOST_TEST_EQ(flatten(s), std::string(1, 'c')); - - // remove 1 -> empty - s.remove_prefix(1); - BOOST_TEST_EQ(buffer_size(s.data()), 0u); - } - - void - testEmpty() - { - // default-constructed slice - detail::slice_impl> s{}; - BOOST_TEST_EQ(buffer_size(s.data()), 0u); - s.remove_prefix(5); - BOOST_TEST_EQ(buffer_size(s.data()), 0u); - } - - void - testMutableVsConst() - { - char a[10]; - std::array mbufs = { - mutable_buffer(a, sizeof(a)) - }; - std::array cbufs = { - const_buffer(a, sizeof(a)) - }; - using m_slice = detail::slice_impl; - using c_slice = detail::slice_impl; - - static_assert( - std::is_same_v, - "mutable input -> mutable buffer_type"); - static_assert( - std::is_same_v, - "const input -> const buffer_type"); - - m_slice ms(mbufs); - c_slice cs(cbufs); - BOOST_TEST_EQ(buffer_size(ms.data()), 10u); - BOOST_TEST_EQ(buffer_size(cs.data()), 10u); - } - - void - testSingleBuffer() - { - char a[10]; - std::memset(a, 'X', sizeof(a)); + // A single buffer is closed under sub-ranging: slicing yields a + // buffer of the same kind, NOT a slice_of wrapper. mutable_buffer mb(a, sizeof(a)); - - detail::slice_impl s(mb); - BOOST_TEST_EQ(buffer_size(s.data()), 10u); - BOOST_TEST_EQ(flatten(s), std::string(10, 'X')); - - s.remove_prefix(3); - BOOST_TEST_EQ(buffer_size(s.data()), 7u); - BOOST_TEST_EQ(flatten(s), std::string(7, 'X')); + auto s = buffer_slice(mb, 2, 5); + static_assert(std::same_as, + "slicing a mutable_buffer yields a mutable_buffer"); + BOOST_TEST_EQ(buffer_size(s), 5u); + BOOST_TEST_EQ(flatten(s), std::string(5, 'A')); + + const_buffer cbuf(a, sizeof(a)); + auto cs = buffer_slice(cbuf, 3); + static_assert(std::same_as, + "slicing a const_buffer yields a const_buffer"); + BOOST_TEST_EQ(buffer_size(cs), 7u); } void - testPublicFunction() + testClampAndEmpty() { char a[10]; - char b[20]; std::memset(a, 'A', sizeof(a)); - std::memset(b, 'B', sizeof(b)); - std::array bufs = { - mutable_buffer(a, sizeof(a)), - mutable_buffer(b, sizeof(b)) - }; - - // default args: whole sequence - { - auto s = buffer_slice(bufs); - static_assert(Slice, - "buffer_slice's return must satisfy Slice"); - static_assert(MutableSlice, - "buffer_slice over mutable input must satisfy MutableSlice"); - BOOST_TEST_EQ(buffer_size(s.data()), 30u); - BOOST_TEST_EQ(flatten(s), - std::string(10, 'A') + std::string(20, 'B')); - } + std::array bufs = { mutable_buffer(a, sizeof(a)) }; - // const input -> Slice but not MutableSlice - { - std::array cbufs = { - const_buffer(a, sizeof(a)) - }; - auto s = buffer_slice(cbufs); - static_assert(Slice, - "buffer_slice over const input must satisfy Slice"); - static_assert(!MutableSlice, - "buffer_slice over const input must NOT satisfy MutableSlice"); - BOOST_TEST_EQ(buffer_size(s.data()), 10u); - } + auto over = buffer_slice(bufs, 4, 999); // length clamps to remaining + BOOST_TEST_EQ(buffer_size(over), 6u); - // offset + length - { - auto s = buffer_slice(bufs, 5, 10); - BOOST_TEST_EQ(buffer_size(s.data()), 10u); - BOOST_TEST_EQ(flatten(s), - std::string(5, 'A') + std::string(5, 'B')); - } - - // offset only (length defaults to "to end") - { - auto s = buffer_slice(bufs, 12); - BOOST_TEST_EQ(buffer_size(s.data()), 18u); - BOOST_TEST_EQ(flatten(s), std::string(18, 'B')); - } - - // single buffer - { - mutable_buffer mb(a, sizeof(a)); - auto s = buffer_slice(mb, 2, 5); - BOOST_TEST_EQ(buffer_size(s.data()), 5u); - BOOST_TEST_EQ(flatten(s), std::string(5, 'A')); - } + auto past = buffer_slice(bufs, 100); // offset past end -> empty + BOOST_TEST_EQ(buffer_size(past), 0u); } void run() { - testConceptModeled(); - testNotABufferSequence(); - testDataIsBufferSequence(); - testWholeSequenceCtor(); - testOffsetLengthCtor(); - testRemovePrefix(); - testRemovePrefixOnLengthCapped(); - testEmpty(); - testMutableVsConst(); - testSingleBuffer(); - testPublicFunction(); + testReturnsBufferSequence(); + testConstInput(); + testOffsetLength(); + testOffsetToEnd(); + testSingleBufferSelfSlice(); + testClampAndEmpty(); } }; -TEST_SUITE(buffer_slice_test, "boost.capy.buffer_slice"); +TEST_SUITE(buffer_slice_test, "boost.capy.buffers.buffer_slice"); + +} // (anon) } // namespace capy } // namespace boost diff --git a/test/unit/buffers/consuming_buffers.cpp b/test/unit/buffers/consuming_buffers.cpp new file mode 100644 index 000000000..a5bebf1a5 --- /dev/null +++ b/test/unit/buffers/consuming_buffers.cpp @@ -0,0 +1,150 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// 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/cppalliance/capy +// + +// Test that the header is self-contained. +#include + +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { + +namespace { + +template +std::string +flatten(Seq const& s) +{ + std::string out; + for (auto it = capy::begin(s); it != capy::end(s); ++it) + { + const_buffer b(*it); + out.append(static_cast(b.data()), b.size()); + } + return out; +} + +struct consuming_buffers_test +{ + void + testNotABufferSequence() + { + char a[4]; + std::array bufs = { mutable_buffer(a, sizeof(a)) }; + using C = consuming_buffers; + // Door 1: the cursor is NOT itself a buffer sequence. + static_assert(!ConstBufferSequence, + "consuming_buffers must not itself model a buffer sequence"); + // Its data() IS a buffer sequence. + static_assert(MutableBufferSequence().data())>, + "consuming_buffers::data() must be a buffer sequence"); + } + + void + testInitialDataIsWhole() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + consuming_buffers consuming(bufs); + BOOST_TEST_EQ(buffer_size(consuming.data()), 30u); + BOOST_TEST_EQ(flatten(consuming.data()), + std::string(10, 'A') + std::string(20, 'B')); + } + + void + testConsumeWithinBuffer() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + consuming_buffers consuming(bufs); + consuming.consume(3); + BOOST_TEST_EQ(buffer_size(consuming.data()), 27u); + BOOST_TEST_EQ(flatten(consuming.data()), + std::string(7, 'A') + std::string(20, 'B')); + } + + void + testConsumeAcrossBuffers() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + consuming_buffers consuming(bufs); + consuming.consume(15); // all of A + 5 of B + BOOST_TEST_EQ(buffer_size(consuming.data()), 15u); + BOOST_TEST_EQ(flatten(consuming.data()), std::string(15, 'B')); + } + + void + testIncrementalConsume() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + // Consume in several steps; data() must track the remainder exactly. + consuming_buffers consuming(bufs); + consuming.consume(4); + consuming.consume(4); + consuming.consume(4); // 12 total: all A + 2 B + BOOST_TEST_EQ(buffer_size(consuming.data()), 18u); + BOOST_TEST_EQ(flatten(consuming.data()), std::string(18, 'B')); + } + + void + testConsumeAllAndClamp() + { + char a[10]; + std::memset(a, 'A', sizeof(a)); + std::array bufs = { mutable_buffer(a, sizeof(a)) }; + + consuming_buffers consuming(bufs); + consuming.consume(1000); // clamped to 10 + BOOST_TEST_EQ(buffer_size(consuming.data()), 0u); + BOOST_TEST(buffer_empty(consuming.data())); + } + + void + run() + { + testNotABufferSequence(); + testInitialDataIsWhole(); + testConsumeWithinBuffer(); + testConsumeAcrossBuffers(); + testIncrementalConsume(); + testConsumeAllAndClamp(); + } +}; + +TEST_SUITE(consuming_buffers_test, "boost.capy.buffers.consuming_buffers"); + +} // (anon) + +} // namespace capy +} // namespace boost diff --git a/test/unit/buffers/front.cpp b/test/unit/buffers/front.cpp deleted file mode 100644 index 78ad9cefb..000000000 --- a/test/unit/buffers/front.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// -// Copyright (c) 2023 Vinnie Falco (vinnie.falco@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/cppalliance/capy -// - -// Test that header file is self-contained. -#include - -#if 0 - -#include -#include -#include -#include -#include - -#include "test_suite.hpp" - -namespace boost { -namespace capy { - -struct front_test -{ - void - testBufferSize() - { - { - char a[7]{}; - char b[11]{}; - const_buffer_pair p( - const_buffer(a, sizeof(a)), - const_buffer(b, sizeof(b))); - BOOST_TEST_EQ( - size(p), - sizeof(a) + sizeof(b)); - } - } - - void - testBufferCopy() - { - std::string const s = - "Howdy partner"; - auto const N = s.size(); - for(std::size_t i = 0; - i < N; ++i) - { - for(std::size_t j = 0; - j < N; ++j) - { - for(std::size_t k = 0; - k < N + 2; ++k) - { - const_buffer_pair p0( - const_buffer( - s.data(), i), - const_buffer( - s.data() + i, N - i)); - char tmp[13]; - std::memset(tmp, 0, sizeof(tmp)); - mutable_buffer_pair p1( - mutable_buffer( - tmp, j), - mutable_buffer( - tmp + j, N - j)); - auto const n = buffer_copy( - p1, p0, k); - BOOST_TEST_LE(n, N); - BOOST_TEST_EQ( - s.substr(0, n), - std::string(tmp, n)); - } - } - } - } - - void - testAlgorithms() - { - // prefix - - { - char buf[16]{}; - const_buffer b(buf, sizeof(buf)); - const_buffer bp = prefix(b, 5); - BOOST_TEST_EQ(bp.size(), 5); - } - - { - char buf[16]{}; - mutable_buffer b(buf, sizeof(buf)); - mutable_buffer bp = prefix(b, 5); - BOOST_TEST_EQ(bp.size(), 5); - } - - // sans_prefix - - { - char buf[16]{}; - const_buffer b(buf, sizeof(buf)); - const_buffer bp = sans_prefix(b, 5); - BOOST_TEST_EQ(bp.size(), 11); - } - - { - char buf[16]{}; - mutable_buffer b(buf, sizeof(buf)); - mutable_buffer bp = sans_prefix(b, 5); - BOOST_TEST_EQ(bp.size(), 11); - } - - // suffix - - { - char buf[16]{}; - const_buffer b(buf, sizeof(buf)); - const_buffer bp = suffix(b, 5); - BOOST_TEST_EQ(bp.size(), 5); - } - - { - char buf[16]{}; - mutable_buffer b(buf, sizeof(buf)); - mutable_buffer bp = suffix(b, 5); - BOOST_TEST_EQ(bp.size(), 5); - } - - // sans_suffix - - { - char buf[16]{}; - const_buffer b(buf, sizeof(buf)); - const_buffer bp = sans_suffix(b, 5); - BOOST_TEST_EQ(bp.size(), 11); - } - - { - char buf[16]{}; - mutable_buffer b(buf, sizeof(buf)); - mutable_buffer bp = sans_suffix(b, 5); - BOOST_TEST_EQ(bp.size(), 11); - } - } - - void - run() - { - testBufferSize(); - testBufferCopy(); - testAlgorithms(); - } -}; - -TEST_SUITE( - front_test, - "boost.capy.buffers.front"); - -} // capy -} // boost - -#endif diff --git a/test/unit/buffers/slice_of.cpp b/test/unit/buffers/slice_of.cpp new file mode 100644 index 000000000..1f8d327f1 --- /dev/null +++ b/test/unit/buffers/slice_of.cpp @@ -0,0 +1,177 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// 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/cppalliance/capy +// + +// Test that the header is self-contained. +#include + +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { + +namespace { + +// Flatten the bytes of a buffer sequence for byte-exact comparison. +template +std::string +flatten(Seq const& s) +{ + std::string out; + for (auto it = capy::begin(s); it != capy::end(s); ++it) + { + const_buffer b(*it); + out.append(static_cast(b.data()), b.size()); + } + return out; +} + +struct slice_of_test +{ + void + testModelsConcept() + { + char a[10]; + std::array mb = { mutable_buffer(a, sizeof(a)) }; + std::array cb = { const_buffer(a, sizeof(a)) }; + + using MS = detail::slice_of; + using CS = detail::slice_of; + + static_assert(MutableBufferSequence, + "slice_of of mutable input must model MutableBufferSequence"); + static_assert(ConstBufferSequence, + "slice_of of mutable input must model ConstBufferSequence"); + static_assert(ConstBufferSequence, + "slice_of of const input must model ConstBufferSequence"); + static_assert(!MutableBufferSequence, + "slice_of of const input must NOT model MutableBufferSequence"); + // It is a plain buffer sequence: no data()/remove_prefix members. + static_assert(std::ranges::bidirectional_range, + "slice_of must be a bidirectional range"); + } + + void + testWholeRange() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + detail::slice_of s(bufs, 0); + BOOST_TEST_EQ(buffer_size(s), 30u); + BOOST_TEST_EQ(flatten(s), std::string(10, 'A') + std::string(20, 'B')); + } + + void + testOffsetLength() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + // [5, 15): last 5 of A, first 5 of B + detail::slice_of s(bufs, 5, 10); + BOOST_TEST_EQ(buffer_size(s), 10u); + BOOST_TEST_EQ(flatten(s), std::string(5, 'A') + std::string(5, 'B')); + } + + void + testOffsetToEnd() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + // offset 12, default length: drop first 12 (all A + 2 B) -> 18 B + detail::slice_of s(bufs, 12); + BOOST_TEST_EQ(buffer_size(s), 18u); + BOOST_TEST_EQ(flatten(s), std::string(18, 'B')); + } + + void + testFrontAndBackTrim() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), mutable_buffer(b, sizeof(b)) }; + + // [3, 27): trims 3 off the front buffer and 3 off the back buffer + detail::slice_of s(bufs, 3, 24); + BOOST_TEST_EQ(buffer_size(s), 24u); + BOOST_TEST_EQ(flatten(s), std::string(7, 'A') + std::string(17, 'B')); + } + + void + testEmptyAndClamp() + { + char a[10]; + std::memset(a, 'A', sizeof(a)); + std::array bufs = { mutable_buffer(a, sizeof(a)) }; + + // offset past the end -> empty + detail::slice_of past(bufs, 100); + BOOST_TEST_EQ(buffer_size(past), 0u); + + // length past the end -> clamped to remaining + detail::slice_of over(bufs, 4, 999); + BOOST_TEST_EQ(buffer_size(over), 6u); + BOOST_TEST_EQ(flatten(over), std::string(6, 'A')); + } + + void + testSkipsEmptyBuffersAtOffset() + { + char a[10], b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + char empty[1]; + std::array bufs = { + mutable_buffer(a, sizeof(a)), + mutable_buffer(empty, 0), + mutable_buffer(b, sizeof(b)) }; + + // offset lands exactly at the boundary after A; empty buffer skipped + detail::slice_of s(bufs, 10); + BOOST_TEST_EQ(buffer_size(s), 20u); + BOOST_TEST_EQ(flatten(s), std::string(20, 'B')); + } + + void + run() + { + testModelsConcept(); + testWholeRange(); + testOffsetLength(); + testOffsetToEnd(); + testFrontAndBackTrim(); + testEmptyAndClamp(); + testSkipsEmptyBuffersAtOffset(); + } +}; + +TEST_SUITE(slice_of_test, "boost.capy.buffers.slice_of"); + +} // (anon) + +} // namespace capy +} // namespace boost diff --git a/test/unit/buffers/test_buffers.hpp b/test/unit/buffers/test_buffers.hpp index ff1f25b03..a30c18d97 100644 --- a/test/unit/buffers/test_buffers.hpp +++ b/test/unit/buffers/test_buffers.hpp @@ -266,34 +266,31 @@ grind_front( for(std::size_t n = 0; n <= pat0.size() + 1; ++n) { { - // remove_prefix: drop the first n bytes + // sans_prefix: drop the first n bytes (value-returning) auto pat = trimmed_front(pat0, n); - auto bs = buffer_slice(bs0); - bs.remove_prefix(n); - check_eq(bs.data(), pat); - check_iterators(bs.data(), pat, tmp); + auto bs = buffer_slice(bs0, n); + check_eq(bs, pat); + check_iterators(bs, pat, tmp); if(deep) { - // Take a copy, blank out the original, and redo the test - auto bsc = bs; - bs = decltype(bs){}; + // Re-slice at increasing offsets: dropping n then m more + // is the same as dropping n + m from the original. for(std::size_t m = 0; m <= pat.size() + 1; ++m) { - auto pat2 = trimmed_front(pat, m); - auto bs2 = bsc; - bs2.remove_prefix(m); - check_eq(bs2.data(), pat2); + auto pat2 = trimmed_front(pat0, n + m); + auto bs2 = buffer_slice(bs0, n + m); + check_eq(bs2, pat2); } } } { - // keep_prefix: keep only the first n bytes + // prefix: keep only the first n bytes auto pat = kept_front(pat0, n); std::size_t const len = (n < total) ? n : total; auto bs = buffer_slice(bs0, 0, len); - check_eq(bs.data(), pat); - check_iterators(bs.data(), pat, tmp); + check_eq(bs, pat); + check_iterators(bs, pat, tmp); } } } @@ -311,42 +308,33 @@ grind_back( for(std::size_t n = 0; n <= pat0.size() + 1; ++n) { { - // remove_suffix: drop the last n bytes + // sans_suffix: drop the last n bytes (keep first total - n) auto pat = trimmed_back(pat0, n); std::size_t const len = (n < total) ? total - n : 0; auto bs = buffer_slice(bs0, 0, len); - check_eq(bs.data(), pat); - check_iterators(bs.data(), pat, tmp); + check_eq(bs, pat); + check_iterators(bs, pat, tmp); if(deep) { - // Take a copy, blank out the original, and redo the test - auto bsc = bs; - bs = decltype(bs){}; + // Dropping the last n then m more is the same as dropping + // the last n + m from the original. for(std::size_t m = 0; m <= pat.size() + 1; ++m) { - auto pat2 = trimmed_back(pat, m); - // Drop another m bytes from the back of bsc by - // length-capping a fresh slice of the same data. - std::size_t const len2 = buffer_size(bsc.data()); + auto pat2 = trimmed_back(pat0, n + m); std::size_t const new_len = - (m < len2) ? len2 - m : 0; - auto bs2 = bsc; - // Walk forward (current state) and use remove_prefix - // to drop the front; for the back we need a fresh - // slice over the inner-window. Easiest: construct - // a new slice from the original at the right offset/length. - bs2 = buffer_slice(bs0, 0, new_len); - check_eq(bs2.data(), pat2); + (n + m < total) ? total - (n + m) : 0; + auto bs2 = buffer_slice(bs0, 0, new_len); + check_eq(bs2, pat2); } } } { - // keep_suffix: keep only the last n bytes + // suffix: keep only the last n bytes auto pat = kept_back(pat0, n); std::size_t const offset = (n < total) ? total - n : 0; auto bs = buffer_slice(bs0, offset); - check_eq(bs.data(), pat); - check_iterators(bs.data(), pat, tmp); + check_eq(bs, pat); + check_iterators(bs, pat, tmp); } } } diff --git a/test/unit/test/bufgrind.cpp b/test/unit/test/bufgrind.cpp index 64ed44d43..b166c27ae 100644 --- a/test/unit/test/bufgrind.cpp +++ b/test/unit/test/bufgrind.cpp @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -59,8 +58,8 @@ class bufgrind_test int count = 0; while(bg) { auto [b1, b2] = co_await bg.next(); - BOOST_TEST_EQ(buffer_size(b1.data()), 0u); - BOOST_TEST_EQ(buffer_size(b2.data()), 0u); + BOOST_TEST_EQ(buffer_size(b1), 0u); + BOOST_TEST_EQ(buffer_size(b2), 0u); ++count; } BOOST_TEST_EQ(count, 1); @@ -81,11 +80,11 @@ class bufgrind_test while(bg) { auto [b1, b2] = co_await bg.next(); if(count == 0) { - BOOST_TEST_EQ(buffer_size(b1.data()), 0u); - BOOST_TEST_EQ(buffer_size(b2.data()), 1u); + BOOST_TEST_EQ(buffer_size(b1), 0u); + BOOST_TEST_EQ(buffer_size(b2), 1u); } else if(count == 1) { - BOOST_TEST_EQ(buffer_size(b1.data()), 1u); - BOOST_TEST_EQ(buffer_size(b2.data()), 0u); + BOOST_TEST_EQ(buffer_size(b1), 1u); + BOOST_TEST_EQ(buffer_size(b2), 0u); } ++count; } @@ -108,9 +107,9 @@ class bufgrind_test while(bg) { auto [b1, b2] = co_await bg.next(); - BOOST_TEST_EQ(buffer_to_string(b1.data(), b2.data()), data); - BOOST_TEST_EQ(buffer_size(b1.data()), static_cast(count)); - BOOST_TEST_EQ(buffer_size(b2.data()), data.size() - count); + BOOST_TEST_EQ(buffer_to_string(b1, b2), data); + BOOST_TEST_EQ(buffer_size(b1), static_cast(count)); + BOOST_TEST_EQ(buffer_size(b2), data.size() - count); ++count; } BOOST_TEST_EQ(count, 6); @@ -130,7 +129,7 @@ class bufgrind_test std::vector positions; while(bg) { auto [b1, b2] = co_await bg.next(); - positions.push_back(buffer_size(b1.data())); + positions.push_back(buffer_size(b1)); } // Expect: 0, 3, 6, 9, 10 (always includes final position) @@ -157,7 +156,7 @@ class bufgrind_test std::vector positions; while(bg) { auto [b1, b2] = co_await bg.next(); - positions.push_back(buffer_size(b1.data())); + positions.push_back(buffer_size(b1)); } // Expect: 0, 2, 4, 6 @@ -201,7 +200,7 @@ class bufgrind_test std::vector positions; while(bg) { auto [b1, b2] = co_await bg.next(); - positions.push_back(buffer_size(b1.data())); + positions.push_back(buffer_size(b1)); } // Expect: 0, 3 (clamped to size, then final) @@ -225,10 +224,10 @@ class bufgrind_test while(bg) { auto [b1, b2] = co_await bg.next(); - // Slices over a mutable input model MutableSlice - static_assert(MutableSlice); - static_assert(MutableSlice); - BOOST_TEST_EQ(buffer_size(b1.data()) + buffer_size(b2.data()), 5u); + // A slice of a mutable input models MutableBufferSequence + static_assert(MutableBufferSequence); + static_assert(MutableBufferSequence); + BOOST_TEST_EQ(buffer_size(b1) + buffer_size(b2), 5u); } }); BOOST_TEST(r.success); @@ -247,13 +246,13 @@ class bufgrind_test while(bg) { auto [b1, b2] = co_await bg.next(); - // Slices over a const-only input model Slice but not - // MutableSlice. - static_assert(Slice); - static_assert(!MutableSlice); - static_assert(Slice); - static_assert(!MutableSlice); - BOOST_TEST_EQ(buffer_size(b1.data()) + buffer_size(b2.data()), 5u); + // A slice of a const-only input models ConstBufferSequence + // but not MutableBufferSequence. + static_assert(ConstBufferSequence); + static_assert(!MutableBufferSequence); + static_assert(ConstBufferSequence); + static_assert(!MutableBufferSequence); + BOOST_TEST_EQ(buffer_size(b1) + buffer_size(b2), 5u); } }); BOOST_TEST(r.success); @@ -278,7 +277,7 @@ class bufgrind_test auto [b1, b2] = co_await bg.next(); // Verify concatenation reconstructs original - BOOST_TEST_EQ(buffer_to_string(b1.data(), b2.data()), "abcdef"); + BOOST_TEST_EQ(buffer_to_string(b1, b2), "abcdef"); ++count; } BOOST_TEST_EQ(count, 7); @@ -301,15 +300,15 @@ class bufgrind_test // Set up read_stream with data matching b1 size read_stream rs(f); - rs.provide(buffer_to_string(b1.data())); + rs.provide(buffer_to_string(b1)); // Read into a destination buffer - if(buffer_size(b1.data()) > 0) { + if(buffer_size(b1) > 0) { std::string dest; - dest.resize(buffer_size(b1.data())); + dest.resize(buffer_size(b1)); auto [ec, n] = co_await rs.read_some(make_buffer(dest)); BOOST_TEST(! ec); - BOOST_TEST_EQ(n, buffer_size(b1.data())); + BOOST_TEST_EQ(n, buffer_size(b1)); } } }); @@ -332,20 +331,20 @@ class bufgrind_test // Write b1 then b2 to stream write_stream ws(f); - if(buffer_size(b1.data()) > 0) { - auto [ec1, n1] = co_await ws.write_some(b1.data()); + if(buffer_size(b1) > 0) { + auto [ec1, n1] = co_await ws.write_some(b1); BOOST_TEST(! ec1); - BOOST_TEST_EQ(n1, buffer_size(b1.data())); + BOOST_TEST_EQ(n1, buffer_size(b1)); } - if(buffer_size(b2.data()) > 0) { - auto [ec2, n2] = co_await ws.write_some(b2.data()); + if(buffer_size(b2) > 0) { + auto [ec2, n2] = co_await ws.write_some(b2); BOOST_TEST(! ec2); - BOOST_TEST_EQ(n2, buffer_size(b2.data())); + BOOST_TEST_EQ(n2, buffer_size(b2)); } // Verify total written equals original - BOOST_TEST_EQ(ws.data(), buffer_to_string(b1.data(), b2.data())); + BOOST_TEST_EQ(ws.data(), buffer_to_string(b1, b2)); } }); BOOST_TEST(r.success); @@ -369,16 +368,16 @@ class bufgrind_test // Write both parts through stream write_stream ws(f); - if(buffer_size(b1.data()) > 0) { - auto [ec, n] = co_await ws.write_some(b1.data()); + if(buffer_size(b1) > 0) { + auto [ec, n] = co_await ws.write_some(b1); BOOST_TEST(! ec); } - if(buffer_size(b2.data()) > 0) { - auto [ec, n] = co_await ws.write_some(b2.data()); + if(buffer_size(b2) > 0) { + auto [ec, n] = co_await ws.write_some(b2); BOOST_TEST(! ec); } - BOOST_TEST_EQ(ws.data(), buffer_to_string(b1.data(), b2.data())); + BOOST_TEST_EQ(ws.data(), buffer_to_string(b1, b2)); BOOST_TEST_EQ(ws.data(), original); } } diff --git a/test/unit/test_dynamic_buffer.hpp b/test/unit/test_dynamic_buffer.hpp index 79c58bea0..5807cf639 100644 --- a/test/unit/test_dynamic_buffer.hpp +++ b/test/unit/test_dynamic_buffer.hpp @@ -50,24 +50,24 @@ grind_dynamic_buffer(F&& make_buffer_fn) while(bg) { auto [b1, b2] = co_await bg.next(); - BOOST_TEST_EQ(buffer_to_string(b1.data(), b2.data()), data); + BOOST_TEST_EQ(buffer_to_string(b1, b2), data); auto db = make_buffer_fn(); // Read b1 into dynamic buffer via read_stream read_stream rs(f); - rs.provide(buffer_to_string(b1.data())); + rs.provide(buffer_to_string(b1)); - if(buffer_size(b1.data()) > 0) + if(buffer_size(b1) > 0) { - auto mb = db.prepare(buffer_size(b1.data())); + auto mb = db.prepare(buffer_size(b1)); auto [ec, n] = co_await rs.read_some(mb); if(ec) co_return; db.commit(n); } - BOOST_TEST_EQ(db.size(), buffer_size(b1.data())); + BOOST_TEST_EQ(db.size(), buffer_size(b1)); // Write from dynamic buffer to write_stream write_stream ws(f); @@ -80,7 +80,7 @@ grind_dynamic_buffer(F&& make_buffer_fn) } // Verify round-trip - BOOST_TEST_EQ(ws.data(), buffer_to_string(b1.data())); + BOOST_TEST_EQ(ws.data(), buffer_to_string(b1)); db.consume(db.size()); BOOST_TEST_EQ(db.size(), 0u);