From a4c48f77f0aa467853b306a7b484d75f68f7a0ae Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Mon, 29 Jun 2026 21:45:06 +0200 Subject: [PATCH] refactor!: remove DynamicBuffer concept and read_until Remove the DynamicBuffer subsystem and the read_until algorithm as a self-contained unit. Nothing in the surviving library consumed these beyond the dynamic read overloads and read_until themselves. Removed: - DynamicBuffer / DynamicBufferParam concepts - read_until and the MatchCondition concept it required (no other consumer) - flat_/circular_/vector_/string_dynamic_buffer types and their factory - the two dynamic read overloads (read-until-eof into a growable buffer) The counted read(stream, MutableBufferSequence) overload is unchanged: it fills a fixed buffer sequence and remains fully covered by tests. BREAKING CHANGE: dynamic buffers, read_until, and the growable-buffer read overloads are no longer part of the public API. --- .cursor/commands/doc-rebuild.md | 12 - doc/modules/ROOT/nav.adoc | 2 - .../ROOT/pages/5.buffers/5f.dynamic.adoc | 348 ----------- .../ROOT/pages/6.streams/6a.overview.adoc | 3 +- .../ROOT/pages/6.streams/6e.algorithms.adoc | 88 --- .../ROOT/pages/6.streams/6f.isolation.adoc | 5 +- .../8.examples/8h.custom-dynamic-buffer.adoc | 301 ---------- .../ROOT/pages/9.design/9b.Separation.adoc | 2 +- .../ROOT/pages/9.design/9c.ReadStream.adoc | 90 +-- .../ROOT/pages/9.design/9d.ReadSource.adoc | 54 -- .../ROOT/pages/9.design/9f.WriteStream.adoc | 55 -- .../ROOT/pages/9.design/9m.WhyNotCobalt.adoc | 18 +- doc/modules/ROOT/pages/why-capy.adoc | 31 +- doc/unlisted/library-buffers.adoc | 33 -- doc/unlisted/library-io-result.adoc | 8 +- doc/unlisted/library-streams.adoc | 5 +- example/CMakeLists.txt | 1 - example/Jamfile | 1 - example/README.md | 4 - example/custom-dynamic-buffer/CMakeLists.txt | 22 - example/custom-dynamic-buffer/Jamfile | 18 - .../custom_dynamic_buffer.cpp | 193 ------ include/boost/capy.hpp | 7 - .../capy/buffers/circular_dynamic_buffer.hpp | 205 ------- .../capy/buffers/flat_dynamic_buffer.hpp | 201 ------- .../capy/buffers/string_dynamic_buffer.hpp | 242 -------- .../capy/buffers/vector_dynamic_buffer.hpp | 251 -------- include/boost/capy/concept/dynamic_buffer.hpp | 212 ------- .../boost/capy/concept/match_condition.hpp | 97 ---- include/boost/capy/cond.hpp | 9 +- include/boost/capy/error.hpp | 3 - include/boost/capy/read.hpp | 246 -------- include/boost/capy/read_until.hpp | 419 -------------- src/buffers/circular_dynamic_buffer.cpp | 93 --- src/cond.cpp | 4 - src/error.cpp | 2 - test/unit/buffers/circular_dynamic_buffer.cpp | 490 ---------------- test/unit/buffers/flat_dynamic_buffer.cpp | 182 ------ test/unit/buffers/string_dynamic_buffer.cpp | 203 ------- test/unit/buffers/vector_dynamic_buffer.cpp | 218 ------- test/unit/concept/dynamic_buffer.cpp | 330 ----------- test/unit/cond.cpp | 17 - test/unit/error.cpp | 2 - test/unit/read.cpp | 510 ---------------- test/unit/read_until.cpp | 547 ------------------ test/unit/test_dynamic_buffer.hpp | 95 --- 46 files changed, 17 insertions(+), 5862 deletions(-) delete mode 100644 doc/modules/ROOT/pages/5.buffers/5f.dynamic.adoc delete mode 100644 doc/modules/ROOT/pages/8.examples/8h.custom-dynamic-buffer.adoc delete mode 100644 example/custom-dynamic-buffer/CMakeLists.txt delete mode 100644 example/custom-dynamic-buffer/Jamfile delete mode 100644 example/custom-dynamic-buffer/custom_dynamic_buffer.cpp delete mode 100644 include/boost/capy/buffers/circular_dynamic_buffer.hpp delete mode 100644 include/boost/capy/buffers/flat_dynamic_buffer.hpp delete mode 100644 include/boost/capy/buffers/string_dynamic_buffer.hpp delete mode 100644 include/boost/capy/buffers/vector_dynamic_buffer.hpp delete mode 100644 include/boost/capy/concept/dynamic_buffer.hpp delete mode 100644 include/boost/capy/concept/match_condition.hpp delete mode 100644 include/boost/capy/read_until.hpp delete mode 100644 src/buffers/circular_dynamic_buffer.cpp delete mode 100644 test/unit/buffers/circular_dynamic_buffer.cpp delete mode 100644 test/unit/buffers/flat_dynamic_buffer.cpp delete mode 100644 test/unit/buffers/string_dynamic_buffer.cpp delete mode 100644 test/unit/buffers/vector_dynamic_buffer.cpp delete mode 100644 test/unit/concept/dynamic_buffer.cpp delete mode 100644 test/unit/read_until.cpp delete mode 100644 test/unit/test_dynamic_buffer.hpp diff --git a/.cursor/commands/doc-rebuild.md b/.cursor/commands/doc-rebuild.md index 4919f8205..fb5bb07b1 100644 --- a/.cursor/commands/doc-rebuild.md +++ b/.cursor/commands/doc-rebuild.md @@ -313,12 +313,6 @@ Even `std::byte` imposes a semantic opinion. POSIX uses `void*` for semantic neu - Scatter/gather operations (multiple buffers in one syscall) - Custom allocators and memory-mapped buffers - Integration with any user-defined buffer type -- **Dynamic Buffers** (`buffers/dynamic.adoc`) - - The producer/consumer model - - The `DynamicBuffer` concept: `prepare(n)`, `commit(n)`, `data()`, `consume(n)` - - Capacity management: `size()`, `max_size()`, `capacity()` - - `DynamicBufferParam` for safe coroutine parameter passing - - Implementations: `flat_dynamic_buffer`, `circular_dynamic_buffer`, `vector_dynamic_buffer`, `string_dynamic_buffer` **Reference headers**: @@ -326,8 +320,6 @@ Even `std::byte` imposes a semantic opinion. POSIX uses `void*` for semantic neu - ``, `` - `` - ``, `` -- `` -- ``, `` ### 6. Stream Concepts (Sixth Section) @@ -361,7 +353,6 @@ Generate content based on public API and agent-guide.md. **Key structure**: For - **Example**: Compression pipeline — source provides compressed data, sink receives decompressed - **Transfer Algorithms** (`streams/algorithms.adoc`) - `read(stream, buffers)` — loops `read_some` until full or error - - `read(source, dynamic_buffer)` — loops until EOF - `write(stream, buffers)` — loops `write_some` until all written - `push_to(BufferSource, WriteSink/WriteStream)` — caller-owns-buffers transfer - `pull_from(ReadSource/ReadStream, BufferSink)` — callee-owns-buffers transfer @@ -428,9 +419,6 @@ Generate examples that showcase Capy features. Examples should progress from sim - Multiple operations in parallel with `when_all` - First-wins pattern with `when_any` - Shows: `when_all`, `when_any`, concurrent composition -- **Custom Dynamic Buffer** (`examples/custom-dynamic-buffer.adoc`) - - Implementing `DynamicBuffer` for a custom allocation strategy - - Shows: concept modeling, `prepare`/`commit`/`consume` pattern - **Echo Server with Corosio** (`examples/echo-server-corosio.adoc`) - Complete echo server using Corosio sockets - Demonstrates Capy + Corosio integration diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index e6e195b7b..4437e2451 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -26,7 +26,6 @@ ** xref:5.buffers/5c.sequences.adoc[Buffer Sequences] ** xref:5.buffers/5d.system-io.adoc[System I/O Integration] ** xref:5.buffers/5e.algorithms.adoc[Buffer Algorithms] -** xref:5.buffers/5f.dynamic.adoc[Dynamic Buffers] * xref:6.streams/6.intro.adoc[Stream Concepts] ** xref:6.streams/6a.overview.adoc[Overview] ** xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] @@ -48,7 +47,6 @@ ** xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] ** xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] ** xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] -** xref:8.examples/8h.custom-dynamic-buffer.adoc[Custom Dynamic Buffer] ** xref:8.examples/8i.echo-server-corosio.adoc[Echo Server with Corosio] ** xref:8.examples/8j.stream-pipeline.adoc[Stream Pipeline] ** xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] diff --git a/doc/modules/ROOT/pages/5.buffers/5f.dynamic.adoc b/doc/modules/ROOT/pages/5.buffers/5f.dynamic.adoc deleted file mode 100644 index 29d8a39c4..000000000 --- a/doc/modules/ROOT/pages/5.buffers/5f.dynamic.adoc +++ /dev/null @@ -1,348 +0,0 @@ -= Dynamic Buffers - -This section introduces dynamic buffers—growable storage that adapts to data flow between producers and consumers. - -== Prerequisites - -* Completed xref:5.buffers/5e.algorithms.adoc[Buffer Algorithms] -* Understanding of buffer sequences and copying - -== The Producer/Consumer Model - -Dynamic buffers serve as intermediate storage between a *producer* (typically network I/O) and a *consumer* (your application code). - -The flow: - -1. **Producer** writes data into the buffer -2. **Buffer** accommodates data within its capacity -3. **Consumer** reads and processes data -4. **Buffer** releases consumed data - -This model decouples production rate from consumption rate—the buffer absorbs variations. - -== The DynamicBuffer Concept - -[source,cpp] ----- -template -concept DynamicBuffer = requires(T& t, T const& ct, std::size_t n) { - typename T::const_buffers_type; - typename T::mutable_buffers_type; - - // Producer side - { t.prepare(n) } -> std::same_as; - { t.commit(n) }; - - // Consumer side - { ct.data() } -> std::same_as; - { t.consume(n) }; - - // Capacity - { ct.size() } -> std::convertible_to; - { ct.max_size() } -> std::convertible_to; - { ct.capacity() } -> std::convertible_to; -}; ----- - -A dynamic buffer, within its capacity provides a potentially empty -_readable region_ and a potentially empty _writeable region_. -Sizes of these regions change as operations are invoked on the -dynamic buffer. - - -== Producer Interface - -=== `prepare(n)` - -Returns a mutable buffer sequence `mb` for writing up to `n` bytes: - -[source,cpp] ----- -capy::MutableBufferSequence auto buffers = dynamic_buf.prepare(1024); // Space for up to 1024 bytes ----- - -_Effects:_ If `n` exceeds the available capacity, throws. Otherwise, -ensures that a writeable region of size at least `n` exists and returns -an object of type `mutable_buffers_type` representing this region. - -_Throws:_ The concrete adapters throw `std::invalid_argument` when `n` -exceeds the available capacity; `circular_dynamic_buffer` throws -`std::length_error`. `std::bad_alloc` may also be thrown if an allocation -is performed and fails. - -_Postcondition:_ `buffer_size(buffers) >= n`. - - -=== `commit(n)` - -Transfers `n` bytes of from the beginning of the writeable storage to -the end of the readable storage: - -[source,cpp] ----- -// After writing data: -dynamic_buf.commit(bytes_written); -// Data is now visible via data() ----- - -Let `n1` be the smaller number of `n` and the size of the writeable region. - -_Effects:_ Removes `n1` bytes from the front of the writeable region and -adds them at the back of the readable region. - -_Notes:_ `n` can be smaller than the writeable region size. - - -=== Typical Producer Pattern - -[source,cpp] ----- -task<> read_into_buffer(Stream& stream, DynamicBuffer auto& buffer) -{ - // Prepare space - auto space = buffer.prepare(1024); - - // Read into prepared space - auto [ec, n] = co_await stream.read_some(space); - - buffer.commit(n); // Make data readable -} ----- - -== Consumer Interface - -=== `data()` - -Returns a const buffer sequence representing the readable region. - -[source,cpp] ----- -capy::ConstBufferSequence auto readable = dynamic_buf.data(); -// Process readable bytes ----- - -_Postcondition:_ `capy::buffer_size(readable) == dynamic_buf.size()`. - - -=== `consume(n)` - -Removes `n` bytes from the front of readable data: - -[source,cpp] ----- -dynamic_buf.consume(processed_bytes); -// Those bytes are no longer in data() ----- - -Let `n1` be the smaller of `n` and `size()`. - -_Effects:_ Removes `n1` bytes from the front of the readable region. -Sets the size of the writeable rgion to zero. Invalidates all buffer -sequences previously obtained via calls to `data()` or `prepare(). - - -=== Typical Consumer Pattern - -[source,cpp] ----- -void process_buffer(DynamicBuffer auto& buffer) -{ - auto data = buffer.data(); - - while (buffer_size(data) >= message_header_size) - { - auto msg_size = parse_header(data); - if (buffer_size(data) < msg_size) - break; // Need more data - - process_message(data, msg_size); - buffer.consume(msg_size); - data = buffer.data(); // Refresh after consume - } -} ----- - -== Capacity Management - -=== `size()` - -_Returns:_ The size of the readable region. - - -=== `max_size()` - -_Returns:_ The maximum possible sum o sizes of the readable region -and the writeable region. - - - -=== `capacity()` - -_Returns:_ Current allocated capacity. - - -=== Class invariant - -`size() +<=+ capacity()`. - -`capacity() +<=+ max_size()`. - -== DynamicBufferParam - -When passing dynamic buffers to coroutines, use `DynamicBufferParam` for safe parameter handling: - -[source,cpp] ----- -template -concept DynamicBufferParam = DynamicBuffer>; - -template -task read_until(Stream& stream, Buf&& buffer, char delimiter); ----- - -This concept ensures proper handling of lvalues and rvalues, preventing dangling references across suspension points. - -== Provided Implementations - -=== flat_dynamic_buffer - -A fixed-capacity adapter over caller-owned contiguous storage, with -single-buffer sequences. It never reallocates: the capacity is fixed at -construction, and a default-constructed buffer has zero capacity (so -`prepare` would throw). - -[source,cpp] ----- -#include - -char storage[1024]; -flat_dynamic_buffer buffer(storage, sizeof(storage)); -auto space = buffer.prepare(256); -// ... write data ... -buffer.commit(n); - -// data() returns a single const_buffer ----- - -Advantages: - -* Contiguous memory—good for parsing that needs contiguous data -* Cache-friendly - -Disadvantages: - -* Fixed capacity; `prepare` throws when more space is requested than remains - -=== circular_dynamic_buffer - -Ring buffer implementation: - -[source,cpp] ----- -#include - -char storage[1024]; -circular_dynamic_buffer buffer(storage, sizeof(storage)); // Fixed capacity ----- - -Advantages: - -* No copying on wrap—head/tail pointers move -* Fixed memory footprint - -Disadvantages: - -* `data()` may return two buffers (wrapped around end) -* Fixed capacity - -=== vector_dynamic_buffer - -Backed by a caller-owned `std::vector` of byte-sized elements -(`vector_dynamic_buffer` itself uses `unsigned char`). Use the -`dynamic_buffer` factory, which deduces the element type: - -[source,cpp] ----- -#include - -std::vector storage; -auto buffer = dynamic_buffer(storage); ----- - -Adapts an existing vector for use as a dynamic buffer. The vector must -outlive the adapter. - -=== string_dynamic_buffer - -Backed by `std::string`: - -[source,cpp] ----- -#include - -std::string storage; -auto buffer = dynamic_buffer(storage); ----- - -Useful when you want the final data as a string. The `dynamic_buffer` -factory wraps the string (you may also construct -`string_dynamic_buffer(&storage)` directly). The string must outlive the -adapter. - -== Example: Line-Based Protocol - -[source,cpp] ----- -task read_line(Stream& stream) -{ - char storage[4096]; - flat_dynamic_buffer buffer(storage, sizeof(storage)); - - while (true) - { - // Prepare space and read - auto space = buffer.prepare(256); - auto [ec, n] = co_await stream.read_some(space); - buffer.commit(n); - if (ec) - throw std::system_error(ec); - - // Search for newline in readable data - auto data = buffer.data(); - std::string_view sv( - static_cast(data.data()), data.size()); - - auto pos = sv.find('\n'); - if (pos != std::string_view::npos) - { - std::string line(sv.substr(0, pos)); - buffer.consume(pos + 1); // Include newline - co_return line; - } - } -} ----- - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| DynamicBuffer concept definition - -| `` -| Linear dynamic buffer - -| `` -| Ring buffer implementation - -| `` -| Vector-backed adapter - -| `` -| String-backed adapter -|=== - -You have now learned about dynamic buffers for producer/consumer patterns. This completes the Buffer Sequences section. Continue to xref:../6.streams/6a.overview.adoc[Stream Concepts] to learn about Capy's stream abstractions. diff --git a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc index 1070bb6cc..53c50208d 100644 --- a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc +++ b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc @@ -4,8 +4,7 @@ This section introduces Capy's stream concepts—the abstractions that enable da == Prerequisites -* Completed xref:../5.buffers/5f.dynamic.adoc[Buffer Sequences] -* Understanding of buffer sequences and the DynamicBuffer concept +* Understanding of buffer sequences == Six Concepts for Data Flow diff --git a/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc b/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc index 37e0f692d..f6895c58c 100644 --- a/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc +++ b/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc @@ -38,42 +38,6 @@ auto [ec, n] = co_await read(stream, make_buffer(buf)); // n == 1024, or ec indicates why not ---- -=== read with DynamicBuffer - -Reads into a growable dynamic buffer, stopping at end-of-stream, when the buffer reaches `max_size()`, or on a non-EOF error: - -[source,cpp] ----- -template -io_task -read(Stream& stream, Buffer&& buffer, std::size_t initial_amount = 2048); ----- - -Example: - -[source,cpp] ----- -std::string storage; -auto buffer = dynamic_buffer(storage); -auto [ec, n] = co_await read(stream, buffer); -// storage holds all data read up to EOF or max_size(); n is the byte count ----- - -=== read from a ReadSource with DynamicBuffer - -A third overload accepts a `ReadSource` and a dynamic buffer. It drives the -source's complete-read `read` (rather than `read_some`), appending until EOF -or until the buffer reaches `max_size()`: - -[source,cpp] ----- -template -io_task -read(Source& source, Buffer&& buffer, std::size_t initial_amount = 2048); ----- - -`n` is the total number of bytes read, inclusive of the final partial read. - === write Writes all data by looping `write_some`: @@ -99,55 +63,6 @@ Example: co_await write(stream, make_buffer("Hello, World!")); ---- -=== read_until - -Reads from a stream into a dynamic buffer until a match condition is -satisfied. Useful for delimiter-based protocols (e.g. reading a line or an -HTTP header block): - -[source,cpp] ----- -#include - -// Match-condition overload -template -io_task -read_until(Stream& stream, Buffer&& buffer, Match match, - std::size_t initial_amount = 2048); - -// Delimiter-string convenience overload -template -io_task -read_until(Stream& stream, Buffer&& buffer, std::string_view delim, - std::size_t initial_amount = 2048); ----- - -If `!ec`, the match succeeded and `n` is the number of bytes through the end -of the match (the position one past the matched delimiter). Notable -conditions: - -* `cond::eof` — end-of-stream reached before a match; `n` is the buffer size -* `cond::not_found` — `max_size()` reached before a match - -A `MatchCondition` is a callable `(std::string_view data, std::size_t* hint)` -returning the position past the match, or `std::string_view::npos` on no -match. When `hint` is non-null it may receive an overlap hint so a delimiter -spanning two reads is not missed. The `match_delim` struct adapts a -`std::string_view` delimiter to this interface and underlies the convenience -overload. - -[source,cpp] ----- -std::string line; -auto [ec, n] = co_await read_until( - stream, string_dynamic_buffer(&line), "\r\n"); -if (ec == cond::eof) - co_return line; // partial line at EOF -if (ec) - throw std::system_error(ec); -line.resize(n - 2); // n includes the "\r\n"; strip it ----- - === write_now `write_now` eagerly writes a complete buffer sequence, attempting to finish @@ -338,9 +253,6 @@ else if (ec) | `` | Composed write operations -| `` -| Read until a match condition or delimiter - | `` | Eager write with frame caching diff --git a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc index fc95a25e7..c885e6ad5 100644 --- a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc +++ b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc @@ -208,8 +208,9 @@ auto response = co_await send_request(conn, { }); // Read body through type-erased source -flat_dynamic_buffer buf; -co_await read(response.body, buf); +char storage[4096]; +mutable_buffer buf(storage, sizeof(storage)); +auto [ec, n] = co_await response.body.read(buf); ---- The HTTP library is isolated from transport details. It compiles once. Users bring their own transport. diff --git a/doc/modules/ROOT/pages/8.examples/8h.custom-dynamic-buffer.adoc b/doc/modules/ROOT/pages/8.examples/8h.custom-dynamic-buffer.adoc deleted file mode 100644 index 62dff0a2e..000000000 --- a/doc/modules/ROOT/pages/8.examples/8h.custom-dynamic-buffer.adoc +++ /dev/null @@ -1,301 +0,0 @@ -= Custom Dynamic Buffer - -Implementing the DynamicBuffer concept for a custom allocation strategy. - -== What You Will Learn - -* Implementing the DynamicBuffer concept -* Understanding `prepare`, `commit`, `consume` lifecycle -* Custom memory management for I/O - -== Prerequisites - -* Completed xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] -* Understanding of dynamic buffers from xref:../5.buffers/5f.dynamic.adoc[Dynamic Buffers] - -== Source Code - -[source,cpp] ----- -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -// Custom dynamic buffer with statistics tracking -class tracked_buffer -{ - std::vector storage_; - std::size_t read_pos_ = 0; // Start of readable data - std::size_t write_pos_ = 0; // End of readable data - std::size_t max_size_; - - // Statistics - std::size_t total_prepared_ = 0; - std::size_t total_committed_ = 0; - std::size_t total_consumed_ = 0; - -public: - explicit tracked_buffer(std::size_t max_size = 65536) - : max_size_(max_size) - { - storage_.reserve(1024); - } - - // === DynamicBuffer interface === - - // Consumer: readable data - capy::const_buffer data() const noexcept - { - return capy::const_buffer( - storage_.data() + read_pos_, - write_pos_ - read_pos_); - } - - // Capacity queries - std::size_t size() const noexcept - { - return write_pos_ - read_pos_; - } - - std::size_t max_size() const noexcept - { - return max_size_; - } - - std::size_t capacity() const noexcept - { - return storage_.capacity() - read_pos_; - } - - // Producer: prepare space for writing - capy::mutable_buffer prepare(std::size_t n) - { - total_prepared_ += n; - - // Compact if needed - if (storage_.size() + n > storage_.capacity() && read_pos_ > 0) - { - compact(); - } - - // Grow if needed - std::size_t required = write_pos_ + n; - if (required > max_size_) - throw std::length_error("tracked_buffer: max_size exceeded"); - - if (required > storage_.size()) - storage_.resize(required); - - return capy::mutable_buffer( - storage_.data() + write_pos_, - n); - } - - // Producer: mark bytes as written - void commit(std::size_t n) - { - total_committed_ += n; - write_pos_ += n; - } - - // Consumer: mark bytes as processed - void consume(std::size_t n) - { - std::size_t actual = std::min(n, size()); // std::size_t - total_consumed_ += actual; - read_pos_ += actual; - - if (read_pos_ == write_pos_) - { - // Buffer empty, reset positions - read_pos_ = 0; - write_pos_ = 0; - } - } - - // === Statistics === - - void print_stats() const - { - std::cout << "Buffer statistics:\n" - << " Total prepared: " << total_prepared_ << " bytes\n" - << " Total committed: " << total_committed_ << " bytes\n" - << " Total consumed: " << total_consumed_ << " bytes\n" - << " Current size: " << size() << " bytes\n" - << " Capacity: " << capacity() << " bytes\n"; - } - -private: - void compact() - { - if (read_pos_ == 0) - return; - - std::size_t len = write_pos_ - read_pos_; - std::memmove(storage_.data(), storage_.data() + read_pos_, len); - read_pos_ = 0; - write_pos_ = len; - } -}; - -// Demonstrate using the custom buffer -capy::task<> read_into_tracked_buffer(capy::test::stream& stream, tracked_buffer& buffer) -{ - // Read data in chunks - while (true) - { - auto space = buffer.prepare(256); // mutable_buffer - // ec: std::error_code, n: std::size_t - auto [ec, n] = co_await stream.read_some(space); - - if (ec == capy::cond::eof) - break; - - if (ec) - throw std::system_error(ec); - - buffer.commit(n); - - std::cout << "Read " << n << " bytes, buffer size now: " - << buffer.size() << "\n"; - } -} - -void demo_tracked_buffer() -{ - std::cout << "=== Tracked Buffer Demo ===\n\n"; - - auto [reader, writer] = capy::test::make_stream_pair(); - writer.provide("Hello, "); - writer.provide("World! "); - writer.provide("This is a test of the custom buffer.\n"); - writer.close(); - - tracked_buffer buffer; - - capy::test::run_blocking()(read_into_tracked_buffer(reader, buffer)); - - std::cout << "\nFinal buffer contents: "; - auto data = buffer.data(); // const_buffer - std::cout.write(static_cast(data.data()), data.size()); - std::cout << "\n\n"; - - buffer.print_stats(); - - // Consume some data - std::cout << "\nConsuming 7 bytes...\n"; - buffer.consume(7); - buffer.print_stats(); -} - -int main() -{ - demo_tracked_buffer(); - return 0; -} ----- - -== Build - -[source,cmake] ----- -add_executable(custom_dynamic_buffer custom_dynamic_buffer.cpp) -target_link_libraries(custom_dynamic_buffer PRIVATE capy) ----- - -== Walkthrough - -=== DynamicBuffer Requirements - -A DynamicBuffer must provide: - -[source,cpp] ----- -// Consumer interface -const_buffer data() const; // Readable data -void consume(std::size_t n); // Mark bytes as processed - -// Producer interface -mutable_buffer prepare(std::size_t n); // Space for writing -void commit(std::size_t n); // Mark bytes as written - -// Capacity queries -std::size_t size() const; // Readable bytes -std::size_t max_size() const; // Maximum allowed -std::size_t capacity() const; // Currently allocated ----- - -=== The Producer/Consumer Flow - -[source,cpp] ----- -// 1. Producer prepares space -auto space = buffer.prepare(256); // mutable_buffer - -// 2. Data is written into space -// ec: std::error_code, n: std::size_t -auto [ec, n] = co_await stream.read_some(space); - -// 3. Producer commits written bytes -buffer.commit(n); - -// 4. Consumer reads data -auto data = buffer.data(); // const_buffer -process(data); - -// 5. Consumer marks bytes as processed -buffer.consume(processed_bytes); ----- - -=== Memory Management - -The `tracked_buffer` implementation: - -* Uses a single contiguous vector -* Tracks read and write positions -* Compacts when needed to reuse space -* Grows on demand up to `max_size` - -== Output - ----- -=== Tracked Buffer Demo === - -Read 51 bytes, buffer size now: 51 - -Final buffer contents: Hello, World! This is a test of the custom buffer. - - -Buffer statistics: - Total prepared: 512 bytes - Total committed: 51 bytes - Total consumed: 0 bytes - Current size: 51 bytes - Capacity: 1024 bytes - -Consuming 7 bytes... -Buffer statistics: - Total prepared: 512 bytes - Total committed: 51 bytes - Total consumed: 7 bytes - Current size: 44 bytes - Capacity: 1017 bytes ----- - -== Exercises - -1. Add a "high water mark" statistic that tracks maximum buffer size reached -2. Implement a ring buffer version that never moves data -3. Add an allocator parameter for custom memory allocation - -== Next Steps - -* xref:8.examples/8i.echo-server-corosio.adoc[Echo Server with Corosio] — Real networking diff --git a/doc/modules/ROOT/pages/9.design/9b.Separation.adoc b/doc/modules/ROOT/pages/9.design/9b.Separation.adoc index 850149b5c..c639ef827 100644 --- a/doc/modules/ROOT/pages/9.design/9b.Separation.adoc +++ b/doc/modules/ROOT/pages/9.design/9b.Separation.adoc @@ -144,7 +144,7 @@ Corosio contains four platform backends, each a substantial body of platform-spe * *kqueue* on macOS and BSD * *select* as a POSIX fallback -Merging these into Capy would mean that a developer who wants a `task<>` type or a `circular_dynamic_buffer` must compile against platform I/O headers. Keeping Capy separate ensures that none of the headers a consumer includes transitively pull in anything from the platform I/O layer. Consumers take only what they need. +Merging these into Capy would mean that a developer who wants a `task<>` type or a `buffer_slice` must compile against platform I/O headers. Keeping Capy separate ensures that none of the headers a consumer includes transitively pull in anything from the platform I/O layer. Consumers take only what they need. == Conclusion diff --git a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc index d1c11f853..102b3a6f1 100644 --- a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc @@ -88,7 +88,7 @@ WriteSink { write_some, write, write_eof(buffers), write_eof() } == Composed Algorithms -Three composed algorithms build on `read_some`: +A composed algorithm builds on `read_some`: === `read(stream, buffers)` -- Fill a Buffer Sequence @@ -117,50 +117,6 @@ task<> read_header(Stream& stream) } ---- -=== `read(stream, dynamic_buffer)` -- Read Until EOF - -[source,cpp] ----- -auto read(ReadStream auto& stream, - DynamicBufferParam auto&& buffers, - std::size_t initial_amount = 2048) - -> io_task; ----- - -Reads from the stream into a dynamic buffer until EOF is reached. The buffer grows with a 1.5x factor when filled. On success (EOF), `ec` is clear and `n` is the total bytes read. - -[source,cpp] ----- -template -task slurp(Stream& stream) -{ - std::string body; - auto [ec, n] = co_await read( - stream, string_dynamic_buffer(&body)); - if(ec) - co_return {}; - co_return body; -} ----- - -=== `read_until(stream, dynamic_buffer, match)` -- Delimited Read - -Reads from the stream into a dynamic buffer until a delimiter or match condition is found. Used for line-oriented protocols and message framing. - -[source,cpp] ----- -template -task<> read_line(Stream& stream) -{ - std::string line; - auto [ec, n] = co_await read_until( - stream, string_dynamic_buffer(&line), "\r\n"); - if(ec) - co_return; - // line contains data up to and including "\r\n" -} ----- - == Use Cases === Incremental Processing with `read_some` @@ -190,46 +146,6 @@ task<> echo(Stream& stream, WriteStream auto& dest) } ---- -=== Relaying from ReadStream to WriteStream - -When relaying data from a reader to a writer, `read_some` feeds `write_some` directly. This is the fundamental streaming pattern. - -[source,cpp] ----- -template -task<> relay(Src& src, Dest& dest) -{ - char storage[65536]; - circular_dynamic_buffer cb(storage, sizeof(storage)); - - for(;;) - { - // Read into free space - auto mb = cb.prepare(cb.capacity()); - auto [rec, nr] = co_await src.read_some(mb); - cb.commit(nr); - - if(rec && rec != cond::eof) - co_return; - - // Drain to destination - while(cb.size() > 0) - { - auto [wec, nw] = co_await dest.write_some( - cb.data()); - if(wec) - co_return; - cb.consume(nw); - } - - if(rec == cond::eof) - co_return; - } -} ----- - -Because `ReadSource` refines `ReadStream`, this relay function also accepts `ReadSource` types. An HTTP body source or a decompressor can be relayed to a `WriteStream` using the same function. - == Relationship to the Write Side [cols="1,1"] @@ -242,9 +158,6 @@ Because `ReadSource` refines `ReadStream`, this relay function also accepts `Rea | `read` free function (composed) | `write_now` (composed, eager) -| `read_until` (composed, delimited) -| No write-side equivalent - | `ReadSource::read` | `WriteSink::write` |=== @@ -331,7 +244,6 @@ No source is forced into an unnatural pattern. Sources that naturally separate d `ReadStream` provides `read_some` as the single partial-read primitive. This is deliberately minimal: - Algorithms that need to fill a buffer completely use the `read` composed algorithm. -- Algorithms that need delimited reads use `read_until`. - Algorithms that need to process data as it arrives use `read_some` directly. - `ReadSource` refines `ReadStream` by adding `read` for complete-read semantics. diff --git a/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc b/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc index 25eaacaf3..1071f5d4e 100644 --- a/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc +++ b/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc @@ -170,22 +170,6 @@ For a decompressor backed by a slow network connection, `read_some` lets you dec | Natural for fixed-size records and structured data |=== -== Composed Algorithms - -=== `read(source, dynamic_buffer)` -- Read Until EOF - -[source,cpp] ----- -auto read(ReadSource auto& source, - DynamicBufferParam auto&& buffers, - std::size_t initial_amount = 2048) - -> io_task; ----- - -Reads from the source into a dynamic buffer until EOF. The buffer grows with a 1.5x factor when filled. On success (EOF), `ec` is clear and `n` is total bytes read. - -This is the `ReadSource` equivalent of the `ReadStream` overload. Both use the same `read` free function name, distinguished by concept constraints. - == Use Cases === Reading an HTTP Body @@ -209,24 +193,6 @@ task read_body(Source& body, std::size_t content_length) } ---- -=== Reading into a Dynamic Buffer - -When the body size is unknown (e.g., chunked encoding), read until EOF using the dynamic buffer overload. - -[source,cpp] ----- -template -task read_chunked_body(Source& body) -{ - std::string result; - auto [ec, n] = co_await read( - body, string_dynamic_buffer(&result)); - if(ec) - co_return {}; - co_return result; -} ----- - === Reading Fixed-Size Records from a Source When a source produces structured records of known size, `read` guarantees each record is completely filled. @@ -326,22 +292,6 @@ Because `ReadSource` refines `ReadStream`, this relay accepts `ReadSource` types The `any_read_source` wrapper type-erases a `ReadSource` behind a virtual interface. This is useful when the concrete source type is not known at compile time. -[source,cpp] ----- -task<> handle_request(any_read_source& body) -{ - // Works for content-length, chunked, - // compressed, or any other source type - std::string data; - auto [ec, n] = co_await read( - body, string_dynamic_buffer(&data)); - if(ec) - co_return; - - process_request(data); -} ----- - == Conforming Types Examples of types that satisfy `ReadSource`: @@ -386,8 +336,4 @@ The `read` operation is a different primitive and uses the complete-read semanti | `read` composed (on `ReadStream`) | Loops `read_some` until the buffer is filled. | Fixed-size headers, known-length messages over raw streams. - -| `read` composed (on `ReadSource`) -| Loops `read` into a dynamic buffer until EOF. -| Slurping an entire body of unknown size. |=== diff --git a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc index 81579f5be..6759c2654 100644 --- a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc @@ -140,60 +140,6 @@ Step 4: [== 16KB ==] write_some --> 16KB (write_now, small payloa Every time `write_now` partially drains a buffer, the remainder is a small payload that wastes a syscall. With top-up, the caller refills the ring buffer between calls, keeping each syscall near capacity. -=== Code: `write_some` with Buffer Top-Up - -This example reads from a `ReadSource` and writes to a `WriteStream` using a `circular_dynamic_buffer`. After each partial write frees space in the ring buffer, the caller reads more data from the source to refill it before calling `write_some` again. - -[source,cpp] ----- -template -task<> relay_with_topup(Source& src, Stream& dest) -{ - char storage[65536]; - circular_dynamic_buffer cb(storage, sizeof(storage)); - - for(;;) - { - // Fill: read from source into free space - auto mb = cb.prepare(cb.capacity()); - auto [rec, nr] = co_await src.read(mb); - cb.commit(nr); - if(rec && rec != cond::eof && nr == 0) - co_return; - - // Drain: write_some from the ring buffer - while(cb.size() > 0) - { - auto [wec, nw] = co_await dest.write_some( - cb.data()); - if(wec) - co_return; - - // consume only what was written - cb.consume(nw); - - // Top-up: refill freed space before next - // write_some, so the next call presents - // the largest possible payload - if(cb.capacity() > 0 && rec != cond::eof) - { - auto mb2 = cb.prepare(cb.capacity()); - auto [rec2, nr2] = co_await src.read(mb2); - cb.commit(nr2); - rec = rec2; - } - // write_some now sees a full (or nearly full) - // ring buffer, maximizing the syscall payload - } - - if(rec == cond::eof) - co_return; - } -} ----- - -After `write_some` accepts 40KB of a 64KB buffer, `consume(40KB)` frees 40KB. The caller immediately reads more data from the source into that freed space. The next `write_some` again presents a full 64KB payload. - === Code: `write_now` Without Top-Up This example reads from a `ReadSource` and writes to a `WriteStream` using `write_now`. Each chunk is drained to completion before the caller can read more from the source. @@ -260,7 +206,6 @@ After the kernel accepts 40KB of a 64KB chunk, `write_now` must send the remaini === Rule of Thumb -- If the caller reads from a source and relays to a raw byte stream (TCP socket), use `write_some` with a `circular_dynamic_buffer` for buffer top-up. - If the caller has a discrete, bounded payload and wants zero-fuss complete-write semantics, use `write_now`. - If the destination is a `WriteSink`, use `write` directly. diff --git a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc index b28a2a6b5..d563c086b 100644 --- a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc +++ b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc @@ -405,9 +405,7 @@ Cobalt uses Asio's `cancellation_signal` and `cancellation_slot`. Propagation is Capy adopts Asio's buffer sequence model — `ConstBufferSequence`, `MutableBufferSequence` — because it works. Capy's buffer types are fully compatible with Asio's. You can pass Capy buffers to Asio operations and vice versa, seamlessly. Then Capy extends the model with additional types and algorithms, while still achieving the Dimovian Ideal — none of this requires exposing Asio headers to consumers. -Cobalt does not provide buffer sequence types or dynamic buffer support. Users who need these features use Asio's types directly, inheriting the `DynamicBuffer_v1`/`DynamicBuffer_v2` split. - -Capy has one `DynamicBuffer` concept. The v1/v2 split in Asio exists because of a fundamental ownership problem: when an async operation takes a buffer by value and completes via callback, who owns the buffer? The original design had flaws, and the fix created two incompatible versions. By going coroutines-only, Capy avoids this entirely. The coroutine frame owns the buffer. Parameters have their lifetimes extended by the suspended frame, and the awaitable lives in the frame alongside them. There is no decay-copy, no ownership transfer, no ambiguity. One concept is sufficient. +Cobalt does not provide buffer sequence types. Users who need these features use Asio's types directly. [cols="1,1,1"] |=== @@ -421,18 +419,6 @@ Capy has one `DynamicBuffer` concept. The v1/v2 split in Asio exists because of | Yes | Via Asio -| `DynamicBuffer` -| Unified -| None (use Asio directly) - -| `flat_dynamic_buffer` -| Yes -| - -| `circular_dynamic_buffer` -| Yes -| - | `front` | Yes | @@ -670,7 +656,7 @@ Every `co_await ws.write(...)` call creates a coroutine frame, suspends, resumes | `cancellation_signal`, automatic, OS-level | Buffer sequences -| Extended, unified `DynamicBuffer` +| Extended (`buffer_slice`, `front`) | None (use Asio directly) | Allocator control diff --git a/doc/modules/ROOT/pages/why-capy.adoc b/doc/modules/ROOT/pages/why-capy.adoc index 8fe3a0268..d687b9e2a 100644 --- a/doc/modules/ROOT/pages/why-capy.adoc +++ b/doc/modules/ROOT/pages/why-capy.adoc @@ -69,7 +69,7 @@ Write `any_stream&` and accept any stream. Your function compiles once. It links * `any_read_stream`, `any_write_stream`, `any_stream` — type-erased partial I/O * `any_read_source`, `any_write_sink` — type-erased complete I/O * `any_buffer_source`, `any_buffer_sink` — type-erased zero-copy -* `read`, `write`, `read_until`, `push_to`, `pull_from` — algorithms that work with erased or concrete streams +* `read`, `write`, `push_to`, `pull_from` — algorithms that work with erased or concrete streams === Comparison @@ -104,9 +104,6 @@ Write `any_stream&` and accept any stream. Your function compiles once. It links | `write` | `async_write`* -| `read_until` -| `async_read_until`* - | `push_to` ^| - @@ -122,18 +119,13 @@ Asio got buffer sequences right. The concept-driven approach—`ConstBufferSeque Capy doesn't reinvent this. We adopt Asio's buffer sequence model because it works. -But we improve on it. Asio provides the basics; Capy extends them. Need to trim bytes from the front of a buffer sequence? Asio makes you work for it. Capy provides `buffer_slice` and `front`—byte-range slicing primitives for efficient byte-level manipulation. Need a circular buffer for protocol parsing? Capy has `circular_dynamic_buffer`. Need to compose two buffers without copying? Use `std::array` (or any range of buffers) directly — Capy's buffer-sequence concepts accept arbitrary ranges. - -And then there's the `DynamicBuffer` mess. If you've used Asio, you've encountered the confusing split between `DynamicBuffer_v1` and `DynamicBuffer_v2`. This exists because of a fundamental problem: when an async operation takes a buffer by value and completes via callback, who owns the buffer? The original design had flaws. The "fix" created two incompatible versions. (See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1100r0.html[P1100R0] for the full story.) - -Coroutines eliminate this problem entirely. The coroutine frame owns the buffer. There's no decay-copy. There's no ownership transfer. The buffer lives in the frame until the coroutine completes. Capy has one `DynamicBuffer` concept. It works. +But we improve on it. Asio provides the basics; Capy extends them. Need to trim bytes from the front of a buffer sequence? Asio makes you work for it. Capy provides `buffer_slice` and `front`—byte-range slicing primitives for efficient byte-level manipulation. Need to compose two buffers without copying? Use `std::array` (or any range of buffers) directly — Capy's buffer-sequence concepts accept arbitrary ranges. One more thing: `std::ranges` cannot help here. `ranges::size` returns the number of buffers, not the total bytes. Range views can drop entire elements, but buffer sequences need byte-level trimming. The abstractions don't match. Buffer sequences need their own concepts. === What Capy Offers -* `ConstBufferSequence`, `MutableBufferSequence`, `DynamicBuffer` — core concepts (Asio-compatible) -* `flat_dynamic_buffer`, `circular_dynamic_buffer` — additional concrete buffer types +* `ConstBufferSequence`, `MutableBufferSequence` — core concepts (Asio-compatible) * `buffer_slice`, `front` — byte-level manipulation utilities === Comparison @@ -148,27 +140,12 @@ One more thing: `std::ranges` cannot help here. `ranges::size` returns the numbe | `MutableBufferSequence` | `MutableBufferSequence` -| `DynamicBuffer` -| `DynamicBuffer_v1`/`v2`* - | `const_buffer` | `const_buffer` | `mutable_buffer` | `mutable_buffer` -| `flat_dynamic_buffer` -^| - - -| `circular_dynamic_buffer` -^| - - -| `vector_dynamic_buffer` -| `dynamic_vector_buffer` - -| `string_dynamic_buffer` -| `dynamic_string_buffer` - | `buffer_slice` ^| - @@ -188,8 +165,6 @@ One more thing: `std::ranges` cannot help here. `ranges::size` returns the numbe ^| - |=== -*Asio has confusing v1/v2 split due to callback composition problems - == Coroutine Execution Model When you write a coroutine, three questions arise immediately. Where does it run? How do you cancel it? How is its frame allocated? diff --git a/doc/unlisted/library-buffers.adoc b/doc/unlisted/library-buffers.adoc index de00e9366..21bfb9372 100644 --- a/doc/unlisted/library-buffers.adoc +++ b/doc/unlisted/library-buffers.adoc @@ -64,9 +64,6 @@ task echo(ReadStream auto& in, WriteStream auto& out) | xref:../buffers/algorithms.adoc[Buffer Algorithms] | `buffer_copy`, `buffer_size`, `buffer_empty` - -| xref:../buffers/dynamic.adoc[Dynamic Buffers] -| Growable buffers with producer/consumer model |=== == Key Types @@ -80,18 +77,6 @@ task echo(ReadStream auto& in, WriteStream auto& out) | `mutable_buffer` | Writable view of contiguous bytes - -| `flat_dynamic_buffer` -| Growable contiguous buffer - -| `circular_dynamic_buffer` -| Ring buffer for streaming - -| `string_dynamic_buffer` -| Adapter for `std::string` - -| `vector_dynamic_buffer` -| Adapter for `std::vector` |=== == Key Concepts @@ -105,12 +90,6 @@ task echo(ReadStream auto& in, WriteStream auto& out) | `MutableBufferSequence` | Range of writable buffers - -| `DynamicBuffer` -| Resizable buffer with prepare/commit semantics - -| `DynamicBufferParam` -| Safe parameter passing for coroutines |=== == Headers @@ -130,18 +109,6 @@ task echo(ReadStream auto& in, WriteStream auto& out) | `` | Byte sub-range slicing algorithm - -| `` -| Contiguous dynamic buffer - -| `` -| Ring buffer implementation - -| `` -| String adapter - -| `` -| Vector adapter |=== == Navigation diff --git a/doc/unlisted/library-io-result.adoc b/doc/unlisted/library-io-result.adoc index 5503c1265..423263190 100644 --- a/doc/unlisted/library-io-result.adoc +++ b/doc/unlisted/library-io-result.adoc @@ -241,16 +241,16 @@ auto [n] = throw_on_error(co_await stream.read_some(buffer)); [source,cpp] ---- -task read_all(stream& s, dynamic_buffer& buf) +task read_all(stream& s, mutable_buffer buf) { - while (true) + while (buf.size() > 0) { - auto [ec, n] = co_await s.read_some(buf.prepare(1024)); + auto [ec, n] = co_await s.read_some(buf); if (ec == error::end_of_stream) co_return; // Normal completion if (ec) throw system_error(ec); - buf.commit(n); + buf += n; // advance past the bytes just read } } ---- diff --git a/doc/unlisted/library-streams.adoc b/doc/unlisted/library-streams.adoc index 9718377cf..e3636c9f0 100644 --- a/doc/unlisted/library-streams.adoc +++ b/doc/unlisted/library-streams.adoc @@ -465,9 +465,6 @@ Free functions build complete operations from partial ones: // Read into fixed buffers (loops read_some until full) auto [ec, n] = co_await read(stream, buffers); - -// Read into dynamic buffer (loops until EOF) -auto [ec, n] = co_await read(source, dynamic_buffer); ---- [source,cpp] @@ -830,4 +827,4 @@ task<> handle_client(tcp_socket& sock) == Navigation [.text-center] -xref:../buffers/dynamic.adoc[<- Dynamic Buffers] | xref:when-all.adoc[Next: Concurrent Composition ->] +xref:../buffers/algorithms.adoc[<- Buffer Algorithms] | xref:when-all.adoc[Next: Concurrent Composition ->] diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 5585a4fd6..79312d723 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -10,7 +10,6 @@ add_subdirectory(async-mutex) add_subdirectory(buffer-composition) -add_subdirectory(custom-dynamic-buffer) add_subdirectory(custom-executor) add_subdirectory(hello-task) add_subdirectory(mock-stream-testing) diff --git a/example/Jamfile b/example/Jamfile index 3850290ea..fe198337b 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -14,7 +14,6 @@ build-project mock-stream-testing ; build-project type-erased-echo ; build-project timeout-cancellation ; build-project parallel-fetch ; -build-project custom-dynamic-buffer ; build-project stream-pipeline ; build-project when-any-cancellation ; build-project asio ; diff --git a/example/README.md b/example/README.md index 09e846e54..5db843c0f 100644 --- a/example/README.md +++ b/example/README.md @@ -32,10 +32,6 @@ Using stop tokens to implement operation timeouts. Running multiple operations concurrently with `when_all`. -### custom-dynamic-buffer/ - -Implementing the DynamicBuffer concept for a custom allocation strategy. - ### echo-server-corosio/ A complete echo server using Corosio for real network I/O. Requires Corosio. diff --git a/example/custom-dynamic-buffer/CMakeLists.txt b/example/custom-dynamic-buffer/CMakeLists.txt deleted file mode 100644 index 4f9bf694f..000000000 --- a/example/custom-dynamic-buffer/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# -# Copyright (c) 2026 Mungo Gill -# -# 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 -# - -file(GLOB_RECURSE PFILES CONFIGURE_DEPENDS *.cpp *.hpp - CMakeLists.txt - Jamfile) - -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) - -add_executable(capy_example_custom_dynamic_buffer ${PFILES}) - -set_property(TARGET capy_example_custom_dynamic_buffer - PROPERTY FOLDER "examples") - -target_link_libraries(capy_example_custom_dynamic_buffer - Boost::capy) diff --git a/example/custom-dynamic-buffer/Jamfile b/example/custom-dynamic-buffer/Jamfile deleted file mode 100644 index e1a05f710..000000000 --- a/example/custom-dynamic-buffer/Jamfile +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (c) 2026 Mungo Gill -# -# 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 -# - -project - : requirements - /boost/capy//boost_capy - . - ; - -exe custom_dynamic_buffer : - [ glob *.cpp ] - ; diff --git a/example/custom-dynamic-buffer/custom_dynamic_buffer.cpp b/example/custom-dynamic-buffer/custom_dynamic_buffer.cpp deleted file mode 100644 index 995ce7ae1..000000000 --- a/example/custom-dynamic-buffer/custom_dynamic_buffer.cpp +++ /dev/null @@ -1,193 +0,0 @@ -// -// Copyright (c) 2026 Mungo Gill -// -// 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 -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -// Custom dynamic buffer with statistics tracking -class tracked_buffer -{ - std::vector storage_; - std::size_t read_pos_ = 0; // Start of readable data - std::size_t write_pos_ = 0; // End of readable data - std::size_t max_size_; - - // Statistics - std::size_t total_prepared_ = 0; - std::size_t total_committed_ = 0; - std::size_t total_consumed_ = 0; - -public: - explicit tracked_buffer(std::size_t max_size = 65536) - : max_size_(max_size) - { - storage_.reserve(1024); - } - - // === DynamicBuffer interface === - - // Consumer: readable data - capy::const_buffer data() const noexcept - { - return capy::const_buffer( - storage_.data() + read_pos_, - write_pos_ - read_pos_); - } - - // Capacity queries - std::size_t size() const noexcept - { - return write_pos_ - read_pos_; - } - - std::size_t max_size() const noexcept - { - return max_size_; - } - - std::size_t capacity() const noexcept - { - return storage_.capacity() - read_pos_; - } - - // Producer: prepare space for writing - capy::mutable_buffer prepare(std::size_t n) - { - total_prepared_ += n; - - // Compact if needed - if (storage_.size() + n > storage_.capacity() && read_pos_ > 0) - { - compact(); - } - - // Grow if needed - std::size_t required = write_pos_ + n; - if (required > max_size_) - throw std::length_error("tracked_buffer: max_size exceeded"); - - if (required > storage_.size()) - storage_.resize(required); - - return capy::mutable_buffer( - storage_.data() + write_pos_, - n); - } - - // Producer: mark bytes as written - void commit(std::size_t n) - { - total_committed_ += n; - write_pos_ += n; - } - - // Consumer: mark bytes as processed - void consume(std::size_t n) - { - std::size_t actual = std::min(n, size()); // std::size_t - total_consumed_ += actual; - read_pos_ += actual; - - if (read_pos_ == write_pos_) - { - // Buffer empty, reset positions - read_pos_ = 0; - write_pos_ = 0; - } - } - - // === Statistics === - - void print_stats() const - { - std::cout << "Buffer statistics:\n" - << " Total prepared: " << total_prepared_ << " bytes\n" - << " Total committed: " << total_committed_ << " bytes\n" - << " Total consumed: " << total_consumed_ << " bytes\n" - << " Current size: " << size() << " bytes\n" - << " Capacity: " << capacity() << " bytes\n"; - } - -private: - void compact() - { - if (read_pos_ == 0) - return; - - std::size_t len = write_pos_ - read_pos_; - std::memmove(storage_.data(), storage_.data() + read_pos_, len); - read_pos_ = 0; - write_pos_ = len; - } -}; - -// Demonstrate using the custom buffer -capy::task<> read_into_tracked_buffer(capy::test::stream& stream, tracked_buffer& buffer) -{ - // Read data in chunks - while (true) - { - auto space = buffer.prepare(256); // mutable_buffer - // ec: std::error_code, n: std::size_t - auto [ec, n] = co_await stream.read_some(space); - - if (ec == capy::cond::eof) - break; - - if (ec) - throw std::system_error(ec); - - buffer.commit(n); - - std::cout << "Read " << n << " bytes, buffer size now: " - << buffer.size() << "\n"; - } -} - -void demo_tracked_buffer() -{ - std::cout << "=== Tracked Buffer Demo ===\n\n"; - - auto [reader, writer] = capy::test::make_stream_pair(); - writer.provide("Hello, "); - writer.provide("World! "); - writer.provide("This is a test of the custom buffer.\n"); - writer.close(); - - tracked_buffer buffer; - - capy::test::run_blocking()(read_into_tracked_buffer(reader, buffer)); - - std::cout << "\nFinal buffer contents: "; - auto data = buffer.data(); // const_buffer - std::cout.write(static_cast(data.data()), data.size()); - std::cout << "\n\n"; - - buffer.print_stats(); - - // Consume some data - std::cout << "\nConsuming 7 bytes...\n"; - buffer.consume(7); - buffer.print_stats(); -} - -int main() -{ - demo_tracked_buffer(); - return 0; -} diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index cdd731f0d..6644b44d3 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -28,7 +28,6 @@ // Algorithms #include #include -#include #include #include #include @@ -39,13 +38,9 @@ #include #include #include -#include #include -#include #include #include -#include -#include // Concepts #include @@ -53,12 +48,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include diff --git a/include/boost/capy/buffers/circular_dynamic_buffer.hpp b/include/boost/capy/buffers/circular_dynamic_buffer.hpp deleted file mode 100644 index 0ede578d0..000000000 --- a/include/boost/capy/buffers/circular_dynamic_buffer.hpp +++ /dev/null @@ -1,205 +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 -// - -#ifndef BOOST_CAPY_BUFFERS_CIRCULAR_DYNAMIC_BUFFER_HPP -#define BOOST_CAPY_BUFFERS_CIRCULAR_DYNAMIC_BUFFER_HPP - -#include -#include -#include - -#include - -namespace boost { -namespace capy { - -/** A fixed-capacity circular buffer satisfying DynamicBuffer. - - This class implements a circular ( ring ) buffer with - fixed capacity determined at construction. Unlike linear - buffers, data can wrap around from the end to the beginning, - enabling efficient FIFO operations without memory copies. - - Buffer sequences returned from @ref data and @ref prepare - may contain up to two elements to represent wrapped regions. - - @par Example - @code - char storage[1024]; - circular_dynamic_buffer cb( storage, sizeof( storage ) ); - - // Write data - auto mb = cb.prepare( 100 ); - std::memcpy( mb.data(), "hello", 5 ); - cb.commit( 5 ); - - // Read data - auto cb_data = cb.data(); - // process cb_data... - cb.consume( 5 ); - @endcode - - @par Thread Safety - Distinct objects: Safe. - Shared objects: Unsafe. - - @see flat_dynamic_buffer, string_dynamic_buffer -*/ -class circular_dynamic_buffer -{ - unsigned char* base_ = nullptr; - std::size_t cap_ = 0; - std::size_t in_pos_ = 0; - std::size_t in_len_ = 0; - std::size_t out_size_ = 0; - -public: - /// Indicates this is a DynamicBuffer adapter over external storage. - using is_dynamic_buffer_adapter = void; - - /// The ConstBufferSequence type for readable bytes. - using const_buffers_type = std::array; - - /// The MutableBufferSequence type for writable bytes. - using mutable_buffers_type = std::array; - - /// Construct an empty circular buffer with zero capacity. - circular_dynamic_buffer() = default; - - /** Construct a copy. - - Copies the adapter state (position and length) but does - not deep-copy the backing storage. Both objects alias the - same external buffer. - - @note The underlying storage must outlive all copies. - */ - circular_dynamic_buffer( - circular_dynamic_buffer const&) = default; - - /** Construct a circular buffer over existing storage. - - @param base Pointer to the storage. - @param capacity Size of the storage in bytes. - */ - circular_dynamic_buffer( - void* base, - std::size_t capacity) noexcept - : base_(static_cast< - unsigned char*>(base)) - , cap_(capacity) - { - } - - /** Construct a circular buffer with initial readable bytes. - - @param base Pointer to the storage. - @param capacity Size of the storage in bytes. - @param initial_size Number of bytes already present as - readable. Must not exceed @p capacity. - - @throws std::invalid_argument if initial_size > capacity. - */ - circular_dynamic_buffer( - void* base, - std::size_t capacity, - std::size_t initial_size) - : base_(static_cast< - unsigned char*>(base)) - , cap_(capacity) - , in_len_(initial_size) - { - if(in_len_ > capacity) - detail::throw_invalid_argument(); - } - - /** Assign by copying. - - Copies the adapter state but does not deep-copy the - backing storage. Both objects alias the same external - buffer afterward. - - @note The underlying storage must outlive all copies. - */ - circular_dynamic_buffer& operator=( - circular_dynamic_buffer const&) = default; - - /// Return the number of readable bytes. - std::size_t - size() const noexcept - { - return in_len_; - } - - /// Return the maximum number of bytes the buffer can hold. - std::size_t - max_size() const noexcept - { - return cap_; - } - - /// Return the number of writable bytes without reallocation. - std::size_t - capacity() const noexcept - { - return cap_ - in_len_; - } - - /// Return a buffer sequence representing the readable bytes. - BOOST_CAPY_DECL - const_buffers_type - data() const noexcept; - - /** Return a buffer sequence for writing. - - Invalidates buffer sequences previously obtained - from @ref prepare. - - @param n The desired number of writable bytes. - - @return A mutable buffer sequence of size @p n. - - @throws std::length_error if `size() + n > max_size()`. - */ - BOOST_CAPY_DECL - mutable_buffers_type - prepare(std::size_t n); - - /** Move bytes from the output to the input sequence. - - Invalidates buffer sequences previously obtained - from @ref prepare. Buffer sequences from @ref data - remain valid. - - @param n The number of bytes to commit. If greater - than the prepared size, all prepared bytes - are committed. - */ - BOOST_CAPY_DECL - void - commit(std::size_t n) noexcept; - - /** Remove bytes from the beginning of the input sequence. - - Invalidates buffer sequences previously obtained - from @ref data. Buffer sequences from @ref prepare - remain valid. - - @param n The number of bytes to consume. If greater - than @ref size(), all readable bytes are consumed. - */ - BOOST_CAPY_DECL - void - consume(std::size_t n) noexcept; -}; - -} // capy -} // boost - -#endif diff --git a/include/boost/capy/buffers/flat_dynamic_buffer.hpp b/include/boost/capy/buffers/flat_dynamic_buffer.hpp deleted file mode 100644 index a487b6898..000000000 --- a/include/boost/capy/buffers/flat_dynamic_buffer.hpp +++ /dev/null @@ -1,201 +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 -// - -#ifndef BOOST_CAPY_BUFFERS_FLAT_DYNAMIC_BUFFER_HPP -#define BOOST_CAPY_BUFFERS_FLAT_DYNAMIC_BUFFER_HPP - -#include -#include -#include - -namespace boost { -namespace capy { - -/** A fixed-capacity linear buffer satisfying DynamicBuffer. - - This class provides a contiguous buffer with fixed capacity - determined at construction. Buffer sequences returned from - @ref data and @ref prepare always contain exactly one element, - making it suitable for APIs requiring contiguous memory. - - @par Example - @code - char storage[1024]; - flat_dynamic_buffer fb( storage, sizeof( storage ) ); - - // Write data - auto mb = fb.prepare( 100 ); - std::memcpy( mb.data(), "hello", 5 ); - fb.commit( 5 ); - - // Read data - auto data = fb.data(); - // process data... - fb.consume( 5 ); - @endcode - - @par Thread Safety - Distinct objects: Safe. - Shared objects: Unsafe. - - @see circular_dynamic_buffer, string_dynamic_buffer -*/ -class flat_dynamic_buffer -{ - unsigned char* data_ = nullptr; - std::size_t cap_ = 0; - std::size_t in_pos_ = 0; - std::size_t in_size_ = 0; - std::size_t out_size_ = 0; - -public: - /// Indicates this is a DynamicBuffer adapter over external storage. - using is_dynamic_buffer_adapter = void; - - /// The ConstBufferSequence type for readable bytes. - using const_buffers_type = const_buffer; - - /// The MutableBufferSequence type for writable bytes. - using mutable_buffers_type = mutable_buffer; - - /// Construct an empty flat buffer with zero capacity. - flat_dynamic_buffer() = default; - - /** Construct a flat buffer over existing storage. - - @param data Pointer to the storage. - @param capacity Size of the storage in bytes. - @param initial_size Number of bytes already present as - readable. Must not exceed @p capacity. - - @throws std::invalid_argument if initial_size > capacity. - */ - flat_dynamic_buffer( - void* data, - std::size_t capacity, - std::size_t initial_size = 0) - : data_(static_cast< - unsigned char*>(data)) - , cap_(capacity) - , in_size_(initial_size) - { - if(in_size_ > cap_) - detail::throw_invalid_argument(); - } - - /// Construct a copy. - flat_dynamic_buffer( - flat_dynamic_buffer const&) = default; - - /// Assign by copying. - flat_dynamic_buffer& operator=( - flat_dynamic_buffer const&) = default; - - /// Return the number of readable bytes. - std::size_t - size() const noexcept - { - return in_size_; - } - - /// Return the maximum number of bytes the buffer can hold. - std::size_t - max_size() const noexcept - { - return cap_; - } - - /// Return the number of writable bytes without reallocation. - std::size_t - capacity() const noexcept - { - return cap_ - (in_pos_ + in_size_); - } - - /// Return a buffer sequence representing the readable bytes. - const_buffers_type - data() const noexcept - { - return const_buffers_type( - data_ + in_pos_, in_size_); - } - - /** Return a buffer sequence for writing. - - Invalidates buffer sequences previously obtained - from @ref prepare. - - @param n The desired number of writable bytes. - - @return A mutable buffer sequence of size @p n. - - @throws std::invalid_argument if `n > capacity()`. - */ - mutable_buffers_type - prepare(std::size_t n) - { - if( n > capacity() ) - detail::throw_invalid_argument(); - - out_size_ = n; - return mutable_buffers_type( - data_ + in_pos_ + in_size_, n); - } - - /** Move bytes from the output to the input sequence. - - Invalidates buffer sequences previously obtained - from @ref prepare. Buffer sequences from @ref data - remain valid. - - @param n The number of bytes to commit. If greater - than the prepared size, all prepared bytes - are committed. - */ - void - commit( - std::size_t n) noexcept - { - if(n < out_size_) - in_size_ += n; - else - in_size_ += out_size_; - out_size_ = 0; - } - - /** Remove bytes from the beginning of the input sequence. - - Invalidates buffer sequences previously obtained - from @ref data. Buffer sequences from @ref prepare - remain valid. - - @param n The number of bytes to consume. If greater - than @ref size(), all readable bytes are consumed. - */ - void - consume( - std::size_t n) noexcept - { - if(n < in_size_) - { - in_pos_ += n; - in_size_ -= n; - } - else - { - in_pos_ = 0; - in_size_ = 0; - } - } -}; - -} // capy -} // boost - -#endif diff --git a/include/boost/capy/buffers/string_dynamic_buffer.hpp b/include/boost/capy/buffers/string_dynamic_buffer.hpp deleted file mode 100644 index 0b98adb35..000000000 --- a/include/boost/capy/buffers/string_dynamic_buffer.hpp +++ /dev/null @@ -1,242 +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 -// - -#ifndef BOOST_CAPY_BUFFERS_STRING_DYNAMIC_BUFFER_HPP -#define BOOST_CAPY_BUFFERS_STRING_DYNAMIC_BUFFER_HPP - -#include -#include -#include -#include - -namespace boost { -namespace capy { - -/** A dynamic buffer backed by a `std::basic_string`. - - This adapter wraps an externally-owned string and - exposes it through the @ref DynamicBuffer interface. - Readable bytes occupy the front of the string; writable - bytes are appended by `prepare` and made readable by - `commit`. - - @note The wrapped string must outlive this adapter. - Calls to `prepare`, `commit`, and `consume` - invalidate previously returned buffer views. - - @par Thread Safety - Distinct objects: Safe. - Shared objects: Unsafe. - - @par Example - @code - std::string s; - auto buf = dynamic_buffer( s, 4096 ); - auto mb = buf.prepare( 100 ); - // fill mb with data... - buf.commit( 100 ); - // buf.data() now has 100 readable bytes - buf.consume( 50 ); - @endcode - - @tparam CharT The character type. - @tparam Traits The character traits type. - @tparam Allocator The allocator type. - - @see DynamicBuffer, string_dynamic_buffer, dynamic_buffer -*/ -template< - class CharT, - class Traits = std::char_traits, - class Allocator = std::allocator> -class basic_string_dynamic_buffer -{ - std::basic_string< - CharT, Traits, Allocator>* s_; - std::size_t max_size_; - - std::size_t in_size_ = 0; - std::size_t out_size_ = 0; - -public: - /// Indicates this is a DynamicBuffer adapter over external storage. - using is_dynamic_buffer_adapter = void; - - /// The underlying string type. - using string_type = std::basic_string< - CharT, Traits, Allocator>; - - /// The ConstBufferSequence type for readable bytes. - using const_buffers_type = const_buffer; - - /// The MutableBufferSequence type for writable bytes. - using mutable_buffers_type = mutable_buffer; - - /// Destroy the buffer. - ~basic_string_dynamic_buffer() = default; - - /// Construct by moving from another buffer. - basic_string_dynamic_buffer( - basic_string_dynamic_buffer&& other) noexcept - : s_(other.s_) - , max_size_(other.max_size_) - , in_size_(other.in_size_) - , out_size_(other.out_size_) - { - other.s_ = nullptr; - } - - /** Construct from an existing string. - - @param s Pointer to the string to wrap. Must - remain valid for the lifetime of this object. - @param max_size Optional upper bound on the number - of bytes the buffer may hold. - */ - explicit - basic_string_dynamic_buffer( - string_type* s, - std::size_t max_size = - std::size_t(-1)) noexcept - : s_(s) - , max_size_( - max_size > s_->max_size() - ? s_->max_size() - : max_size) - { - if(s_->size() > max_size_) - s_->resize(max_size_); - in_size_ = s_->size(); - } - - /// Copy assignment is deleted. - basic_string_dynamic_buffer& operator=( - basic_string_dynamic_buffer const&) = delete; - - /// Return the number of readable bytes. - std::size_t - size() const noexcept - { - return in_size_; - } - - /// Return the maximum number of bytes the buffer can hold. - std::size_t - max_size() const noexcept - { - return max_size_; - } - - /// Return the number of writable bytes without reallocation. - std::size_t - capacity() const noexcept - { - if(s_->capacity() <= max_size_) - return s_->capacity() - in_size_; - return max_size_ - in_size_; - } - - /// Return a buffer sequence representing the readable bytes. - const_buffers_type - data() const noexcept - { - return const_buffers_type( - s_->data(), in_size_); - } - - /** Prepare writable space of at least `n` bytes. - - Invalidates iterators and references returned by - previous calls to `data` and `prepare`. - - @throws std::invalid_argument if `n` exceeds - available space. - - @param n The number of bytes to prepare. - - @return A mutable buffer of exactly `n` bytes. - */ - mutable_buffers_type - prepare(std::size_t n) - { - // n exceeds available space - if(n > max_size_ - in_size_) - detail::throw_invalid_argument(); - - if( s_->size() < in_size_ + n) - s_->resize(in_size_ + n); - out_size_ = n; - return mutable_buffers_type( - &(*s_)[in_size_], out_size_); - } - - /** Move bytes from the writable to the readable area. - - Invalidates iterators and references returned by - previous calls to `data` and `prepare`. - - @param n The number of bytes to commit. Clamped - to the size of the writable area. - */ - void commit(std::size_t n) noexcept - { - if(n < out_size_) - in_size_ += n; - else - in_size_ += out_size_; - out_size_ = 0; - s_->resize(in_size_); - } - - /** Remove bytes from the beginning of the readable area. - - Invalidates iterators and references returned by - previous calls to `data` and `prepare`. - - @param n The number of bytes to consume. Clamped - to the number of readable bytes. - */ - void consume(std::size_t n) noexcept - { - if(n < in_size_) - { - s_->erase(0, n); - in_size_ -= n; - } - else - { - s_->clear(); - in_size_ = 0; - } - out_size_ = 0; - } -}; - -/// A dynamic buffer using `std::string`. -using string_dynamic_buffer = basic_string_dynamic_buffer; - -/** Create a dynamic buffer from a string. - - @param s The string to wrap. - @param max_size Optional maximum size limit. - @return A string_dynamic_buffer wrapping the string. -*/ -template -basic_string_dynamic_buffer -dynamic_buffer( - std::basic_string& s, - std::size_t max_size = std::size_t(-1)) -{ - return basic_string_dynamic_buffer(&s, max_size); -} - -} // capy -} // boost - -#endif diff --git a/include/boost/capy/buffers/vector_dynamic_buffer.hpp b/include/boost/capy/buffers/vector_dynamic_buffer.hpp deleted file mode 100644 index 05c8366cd..000000000 --- a/include/boost/capy/buffers/vector_dynamic_buffer.hpp +++ /dev/null @@ -1,251 +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 -// - -#ifndef BOOST_CAPY_BUFFERS_VECTOR_DYNAMIC_BUFFER_HPP -#define BOOST_CAPY_BUFFERS_VECTOR_DYNAMIC_BUFFER_HPP - -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { - -/** A dynamic buffer using an underlying vector. - - This class adapts a `std::vector` of byte-sized elements - to satisfy the DynamicBuffer concept. The vector provides - automatic memory management and growth. - - @par Constraints - - The element type `T` must be a fundamental type with - `sizeof( T ) == 1`. This includes `char`, `unsigned char`, - `signed char`, and similar byte-sized fundamental types. - - @par Example - @code - std::vector v; - vector_dynamic_buffer vb( &v ); - - // Write data - auto mb = vb.prepare( 100 ); - std::memcpy( mb.data(), "hello", 5 ); - vb.commit( 5 ); - - // Read data - auto data = vb.data(); - // process data... - vb.consume( 5 ); - @endcode - - @par Thread Safety - Distinct objects: Safe. - Shared objects: Unsafe. - - @tparam T The element type. Must be fundamental with sizeof 1. - @tparam Allocator The allocator type for the vector. - - @see flat_dynamic_buffer, circular_dynamic_buffer, string_dynamic_buffer -*/ -template< - class T, - class Allocator = std::allocator> - requires std::is_fundamental_v && (sizeof(T) == 1) -class basic_vector_dynamic_buffer -{ - std::vector* v_; - std::size_t max_size_; - - std::size_t in_size_ = 0; - std::size_t out_size_ = 0; - -public: - /// Indicates this is a DynamicBuffer adapter over external storage. - using is_dynamic_buffer_adapter = void; - - /// The underlying vector type. - using vector_type = std::vector; - - /// The ConstBufferSequence type for readable bytes. - using const_buffers_type = const_buffer; - - /// The MutableBufferSequence type for writable bytes. - using mutable_buffers_type = mutable_buffer; - - /// Destroy the buffer. - ~basic_vector_dynamic_buffer() = default; - - /** Construct by moving. - */ - basic_vector_dynamic_buffer( - basic_vector_dynamic_buffer&& other) noexcept - : v_(other.v_) - , max_size_(other.max_size_) - , in_size_(other.in_size_) - , out_size_(other.out_size_) - { - other.v_ = nullptr; - } - - /** Construct a dynamic buffer over a vector. - - @param v Pointer to the vector to use as storage. - @param max_size Optional maximum size limit. Defaults - to the vector's `max_size()`. - */ - explicit - basic_vector_dynamic_buffer( - vector_type* v, - std::size_t max_size = - std::size_t(-1)) noexcept - : v_(v) - , max_size_( - max_size > v_->max_size() - ? v_->max_size() - : max_size) - { - if(v_->size() > max_size_) - v_->resize(max_size_); - in_size_ = v_->size(); - } - - /// Copy assignment is deleted. - basic_vector_dynamic_buffer& operator=( - basic_vector_dynamic_buffer const&) = delete; - - /// Return the number of readable bytes. - std::size_t - size() const noexcept - { - return in_size_; - } - - /// Return the maximum number of bytes the buffer can hold. - std::size_t - max_size() const noexcept - { - return max_size_; - } - - /// Return the number of writable bytes without reallocation. - std::size_t - capacity() const noexcept - { - if(v_->capacity() <= max_size_) - return v_->capacity() - in_size_; - return max_size_ - in_size_; - } - - /// Return a buffer sequence representing the readable bytes. - const_buffers_type - data() const noexcept - { - return const_buffers_type( - v_->data(), in_size_); - } - - /** Return a buffer sequence for writing. - - Invalidates buffer sequences previously obtained - from @ref prepare. - - @param n The desired number of writable bytes. - - @return A mutable buffer sequence of size @p n. - - @throws std::invalid_argument if `size() + n > max_size()`. - */ - mutable_buffers_type - prepare(std::size_t n) - { - if(n > max_size_ - in_size_) - detail::throw_invalid_argument(); - - if(v_->size() < in_size_ + n) - v_->resize(in_size_ + n); - out_size_ = n; - return mutable_buffers_type( - v_->data() + in_size_, out_size_); - } - - /** Move bytes from the output to the input sequence. - - Invalidates buffer sequences previously obtained - from @ref prepare. Buffer sequences from @ref data - remain valid. - - @param n The number of bytes to commit. If greater - than the prepared size, all prepared bytes - are committed. - */ - void - commit(std::size_t n) noexcept - { - if(n < out_size_) - in_size_ += n; - else - in_size_ += out_size_; - out_size_ = 0; - v_->resize(in_size_); - } - - /** Remove bytes from the beginning of the input sequence. - - Invalidates buffer sequences previously obtained - from @ref data. Buffer sequences from @ref prepare - remain valid. - - @param n The number of bytes to consume. If greater - than @ref size(), all readable bytes are consumed. - */ - void - consume(std::size_t n) noexcept - { - if(n < in_size_) - { - v_->erase(v_->begin(), v_->begin() + n); - in_size_ -= n; - } - else - { - v_->clear(); - in_size_ = 0; - } - out_size_ = 0; - } -}; - -/// A dynamic buffer using `std::vector`. -using vector_dynamic_buffer = - basic_vector_dynamic_buffer; - -/** Create a dynamic buffer from a vector. - - @param v The vector to wrap. Element type must be - a fundamental type with sizeof 1. - @param max_size Optional maximum size limit. - @return A vector_dynamic_buffer wrapping the vector. -*/ -template - requires std::is_fundamental_v && (sizeof(T) == 1) -basic_vector_dynamic_buffer -dynamic_buffer( - std::vector& v, - std::size_t max_size = std::size_t(-1)) -{ - return basic_vector_dynamic_buffer(&v, max_size); -} - -} // capy -} // boost - -#endif diff --git a/include/boost/capy/concept/dynamic_buffer.hpp b/include/boost/capy/concept/dynamic_buffer.hpp deleted file mode 100644 index 9d6b5e465..000000000 --- a/include/boost/capy/concept/dynamic_buffer.hpp +++ /dev/null @@ -1,212 +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 -// - -#ifndef BOOST_CAPY_CONCEPT_DYNAMIC_BUFFER_HPP -#define BOOST_CAPY_CONCEPT_DYNAMIC_BUFFER_HPP - -#include -#include - -#include -#include - -/* - LLM INSTRUCTION: Passing Dynamic Buffers Correctly - - There are two kinds of dynamic buffer types: - - 1. VALUE TYPES (a user-defined buffer that owns its storage, - e.g. one holding a std::array member) - - Store the bytes and bookkeeping inside the object itself - - MUST be passed by lvalue reference to preserve state - - Passing as rvalue loses the data on coroutine suspend - - 2. WRAPPER ADAPTERS (all of Capy's provided types: - flat_dynamic_buffer, circular_dynamic_buffer, - vector_dynamic_buffer, string_dynamic_buffer) - - Reference external storage (a caller-owned array, - std::string, or std::vector) - - Safe to pass as rvalues; the external object retains data - - Define `using is_dynamic_buffer_adapter = void;` - - When writing functions: - - - NON-COROUTINE: Use `DynamicBuffer auto&` (lvalue ref) - void fill(DynamicBuffer auto& buffers); - - - COROUTINE: Use `DynamicBufferParam auto&&` (forwarding ref) - io_task read(DynamicBufferParam auto&& buffers); - - DynamicBufferParam enforces safe passing at compile time: - accepts lvalues of any DynamicBuffer, but rvalues only for adapters. - - WRONG patterns that compile but misbehave: - - Using DynamicBuffer in coroutines (allows dangerous rvalues) - - Using lvalue ref with DynamicBufferParam (rejects valid adapters) -*/ - -namespace boost { -namespace capy { - -/** Concept for resizable buffer types with prepare/commit semantics. - - Types satisfying this concept provide a two-phase write model: - call `prepare(n)` to get writable space, write data, then call - `commit(n)` to make those bytes readable via `data()`. - - @par Semantic Requirements - - `data()` returns buffer sequence valid until next mutating operation - - `prepare(n)` returns buffer sequence valid until `commit()` or next `prepare()` - - Types may reference external storage; caller manages lifetime - - @par Value Types vs Wrapper Adapters - Dynamic buffer types fall into two categories: - - - **Value types** store the bytes and bookkeeping inside the object. - Passing one as an rvalue to a coroutine loses state on suspend. Capy - ships no value types; this category is for user-defined buffers that - own their storage internally. - - - **Wrapper adapters** reference external storage and are safe as rvalues - since the external object persists. All of Capy's provided dynamic - buffers are adapters: `flat_dynamic_buffer` and `circular_dynamic_buffer` - (over a caller-owned array), `vector_dynamic_buffer`, and - `string_dynamic_buffer`. Each defines `using is_dynamic_buffer_adapter = void;`. - - @par Conforming Signatures - For **non-coroutine** functions, use `DynamicBuffer auto&`: - @code - void fill( DynamicBuffer auto& buffers ); - @endcode - - For **coroutine** functions, use `DynamicBufferParam auto&&` instead. - This concept enforces lifetime safety: it accepts lvalues of any - DynamicBuffer, but restricts rvalues to adapter types only. Using - plain `DynamicBuffer` in coroutines allows dangerous rvalue passing - that compiles but silently loses data on suspend. - @code - io_task - read( ReadSource auto& src, DynamicBufferParam auto&& buffers ); - @endcode - - @par Example - @code - flat_dynamic_buffer fb( storage, sizeof( storage ) ); - auto mb = fb.prepare( 100 ); // get writable region - std::size_t n = read( sock, mb ); - fb.commit( n ); // make n bytes readable - process( fb.data() ); // access committed data - fb.consume( fb.size() ); // discard processed data - @endcode - - @see DynamicBufferParam -*/ -template -concept DynamicBuffer = - requires(T& t, T const& ct, std::size_t n) - { - typename T::const_buffers_type; - typename T::mutable_buffers_type; - { ct.size() } -> std::convertible_to; - { ct.max_size() } -> std::convertible_to; - { ct.capacity() } -> std::convertible_to; - { ct.data() } -> std::same_as; - { t.prepare(n) } -> std::same_as; - t.commit(n); - t.consume(n); - } && - ConstBufferSequence && - MutableBufferSequence; - -/** Concept for valid DynamicBuffer parameter passing to coroutines. - - This concept constrains how a DynamicBuffer type can be passed - to coroutine-based I/O functions. It allows: - - - **Lvalues** of any DynamicBuffer (caller manages lifetime) - - **Rvalues** only for types with `is_dynamic_buffer_adapter` tag - - The distinction exists because a value type stores its bytes and - bookkeeping internally, which would be lost if passed by rvalue, while - adapters (such as Capy's `flat_dynamic_buffer` or `string_dynamic_buffer`) - update external storage directly. - - @par Conforming Signatures - For coroutine functions, use a forwarding reference: - @code - io_task - read( ReadSource auto& source, DynamicBufferParam auto&& buffers ); - @endcode - - The forwarding reference is essential because the concept inspects - the value category to enforce the lvalue/rvalue rules. Using the - wrong reference type causes incorrect behavior: - @code - // owning_buffer is a user-defined value type: it owns its storage and - // has no is_dynamic_buffer_adapter tag. ob is an lvalue of it. - owning_buffer ob; - - // WRONG: lvalue ref rejects valid rvalue adapters - void bad1( DynamicBufferParam auto& buffers ); - bad1( ob ); // OK - bad1( string_dynamic_buffer( &s ) ); // compile error, but should work - - // WRONG: const ref deduces non-reference, rejects non-adapters - void bad2( DynamicBufferParam auto const& buffers ); - bad2( ob ); // compile error, but should work - bad2( string_dynamic_buffer( &s ) ); // OK (adapter only) - - // CORRECT: forwarding ref enables proper checking - void good( DynamicBufferParam auto&& buffers ); - good( ob ); // OK: lvalue value type - good( string_dynamic_buffer( &s ) ); // OK: adapter rvalue - good( owning_buffer{} ); // compile error: non-adapter rvalue - @endcode - - @par Adapter Types - Types safe to pass as rvalues define a nested tag: - @code - class string_dynamic_buffer { - public: - using is_dynamic_buffer_adapter = void; - // ... - }; - @endcode - - @par Example - @code - // OK: lvalue reference (any DynamicBuffer) - char storage[1024]; - flat_dynamic_buffer fb( storage, sizeof( storage ) ); - co_await read( stream, fb ); - - // OK: adapter as rvalue — flat references the caller's array, - // which persists across the suspend - co_await read( stream, flat_dynamic_buffer( storage, sizeof( storage ) ) ); - - // OK: adapter as rvalue — the string retains the data - std::string s; - co_await read( stream, string_dynamic_buffer( &s ) ); - - // ERROR: a user-defined value type as rvalue loses state on suspend - co_await read( stream, owning_buffer{} ); // compile error: non-adapter rvalue - @endcode - - @see DynamicBuffer -*/ -template -concept DynamicBufferParam = - DynamicBuffer> && - (std::is_lvalue_reference_v || - requires { typename std::remove_cvref_t::is_dynamic_buffer_adapter; }); - -} // capy -} // boost - -#endif diff --git a/include/boost/capy/concept/match_condition.hpp b/include/boost/capy/concept/match_condition.hpp deleted file mode 100644 index ce9f0f883..000000000 --- a/include/boost/capy/concept/match_condition.hpp +++ /dev/null @@ -1,97 +0,0 @@ -// -// Copyright (c) 2025 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 -// - -#ifndef BOOST_CAPY_CONCEPT_MATCH_CONDITION_HPP -#define BOOST_CAPY_CONCEPT_MATCH_CONDITION_HPP - -#include -#include -#include -#include - -namespace boost { -namespace capy { - -/** Concept for callables that detect delimiters in streamed data. - - A type satisfies `MatchCondition` if it is callable with - `std::string_view` and a `std::size_t*` hint parameter, - returning the position after a match or `npos` if not found. - Used by `read_until` to scan accumulated data for delimiters. - - @tparam F The callable type. - - @par Syntactic Requirements - - @li `f(data, hint)` must be a valid expression where: - - `data` is `std::string_view` - - `hint` is `std::size_t*` (may be null) - @li The return type must be convertible to `std::size_t` - - @par Semantic Requirements - - The callable scans `data` for a delimiter or pattern: - - @li On match: returns position after the match (bytes to consume) - @li On no match: returns `std::string_view::npos` - - The `hint` parameter enables efficient cross-boundary searching: - - @li If `hint` is null, the matcher ignores it - @li If `hint` is non-null and no match is found, the matcher may - write how many bytes from the end might be part of a partial - match (e.g., 3 for "\r\n\r" when searching for "\r\n\r\n") - @li The hint allows the caller to retain only necessary bytes - when the buffer must be compacted - - @par Conforming Signatures - - @code - std::size_t operator()( std::string_view data, std::size_t* hint ) const; - @endcode - - @par Example - - @code - // Simple line matcher (ignores hint) - auto line_matcher = []( std::string_view data, std::size_t* ) { - auto pos = data.find( "\r\n" ); - return pos != std::string_view::npos ? pos + 2 : pos; - }; - - // HTTP header end matcher with overlap hint - struct http_header_matcher - { - std::size_t operator()( - std::string_view data, - std::size_t* hint ) const noexcept - { - auto pos = data.find( "\r\n\r\n" ); - if( pos != std::string_view::npos ) - return pos + 4; - if( hint ) - *hint = 3; // "\r\n\r" might span reads - return std::string_view::npos; - } - }; - - static_assert( MatchCondition ); - @endcode - - @see read_until -*/ -template -concept MatchCondition = requires(F f, std::string_view data, std::size_t* hint) { - { f(data, hint) } -> std::convertible_to; -}; - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/cond.hpp b/include/boost/capy/cond.hpp index fd369af24..430a35740 100644 --- a/include/boost/capy/cond.hpp +++ b/include/boost/capy/cond.hpp @@ -64,19 +64,12 @@ enum class cond */ stream_truncated = 3, - /** Item not found condition. - - An `error_code` compares equal to `not_found` when a - lookup operation failed to find the requested item. - */ - not_found = 4, - /** Operation timed out condition. An `error_code` compares equal to `timeout` when an operation exceeded its allowed duration. */ - timeout = 5 + timeout = 4 }; } // capy diff --git a/include/boost/capy/error.hpp b/include/boost/capy/error.hpp index 02833c47a..41f8c20e1 100644 --- a/include/boost/capy/error.hpp +++ b/include/boost/capy/error.hpp @@ -43,9 +43,6 @@ enum class error /// Compare with `cond::stream_truncated`. stream_truncated, - /// Requested item was not found. Compare with `cond::not_found`. - not_found, - /// Operation timed out. Compare with `cond::timeout`. timeout }; diff --git a/include/boost/capy/read.hpp b/include/boost/capy/read.hpp index 3abcdf280..01839f229 100644 --- a/include/boost/capy/read.hpp +++ b/include/boost/capy/read.hpp @@ -15,10 +15,7 @@ #include #include #include -#include -#include #include -#include #include #include @@ -114,249 +111,6 @@ read(S& stream, MB buffers) -> co_return {{}, total_read}; } -namespace detail { - -// Clamp a prepare request for the dynamic-buffer read loops. -// -// `available` is `max_size() - size()`. The request is clamped to it so that -// `size() + request` never exceeds `max_size()`. A degenerate `initial_amount` -// of 0 would otherwise make `prepare(0)` return an empty buffer and spin -// forever with no progress, so the result is floored to 1. -inline -std::size_t -read_prepare_amount( - std::size_t amount, - std::size_t available) noexcept -{ - std::size_t const n = (std::min)(amount, available); - return n != 0 ? n : 1; -} - -} // namespace detail - -/** Read all data from a stream into a dynamic buffer. - - @par Await-effects - - Reads data from `stream` via awaiting `stream.read_some` repeatedly - and appending it to `dynbuf` using prepare/commit semantics - until: - - @li either @c dynbuf.size() == @c dynbuf.max_size() , - @li or a contingency on @c stream.read_some occurs. - - The last, potenitally partial, read is also appended. - - The value passed in the first call to `dynbuf.prepare` is `initial_amount`. - The value is grown to 1.5 times the preceding value only after a read that - completely filled the prepared buffer; otherwise it is left unchanged for - the next call. - - - @par Await-returns - - An object of type `io_result` destructuring as `[ec, n]`. - - `n` represents the total number of bytes read, - inclusive of the last partial read. - - Contingencies: - - @li The first contingency, other than one matching to @c cond::eof, reported from awaiting @c stream.read_some . - - - @par Await-throws - - Whatever operations on @c dynbuf throw. - - This algorithm relies on @c dynbuf.prepare(n) accepting any @c n for which - `dynbuf.size() + n <= dynbuf.max_size()`. The growable buffers - (@ref string_dynamic_buffer , @ref vector_dynamic_buffer ) honor this by - reallocating, and @ref circular_dynamic_buffer by wrapping. A fixed-capacity - buffer is permitted, but not required, to compact; one that does not can - have `capacity() < max_size() - size()` after a partial @c consume (e.g. a - reused @ref flat_dynamic_buffer ). Preparing into that gap throws - (@c std::invalid_argument or @c std::length_error ), so such a buffer must be - passed without a previously consumed prefix. Allocation failure in a growable - buffer surfaces as @c std::bad_alloc . - - - @param stream The stream to read from. If the lifetime of `stream` ends - before the coroutine finishes, the behavior is undefined. - - @param dynbuf The dynamic buffer to append data to. If the lifetime of the buffer - sequence represented by `dynbuf` ends before the coroutine finishes, the behavior is undefined. - - @param initial_amount Hint for the value passed to `dynbuf.prepare()` - (default 2048; a value of 0 is treated as 1). There is no precondition - relating `initial_amount` to `dynbuf.max_size()`: the requested amount is - clamped so that `dynbuf.size()` plus the request never exceeds - `dynbuf.max_size()`. Reaching `max_size()` completes the operation - successfully. - - - @par Remarks - Supports _IoAwaitable cancellation_. - - @par Example - - @code - capy::task read_body(capy::ReadStream auto& stream) - { - std::string body; - auto [ec, n] = co_await capy::read(stream, capy::dynamic_buffer(body)); - if (ec) - throw std::system_error(ec); - return body; - } - @endcode - - @see read_some, ReadStream, DynamicBufferParam -*/ -template - requires ReadStream && DynamicBufferParam -auto -read( - S& stream, - DB&& dynbuf, - std::size_t initial_amount = 2048) -> - io_task -{ - std::size_t amount = initial_amount; - std::size_t total_read = 0; - for(;;) - { - if(dynbuf.size() >= dynbuf.max_size()) - co_return {{}, total_read}; - - std::size_t const available = dynbuf.max_size() - dynbuf.size(); - auto mb = dynbuf.prepare( - detail::read_prepare_amount(amount, available)); - auto const mb_size = buffer_size(mb); - auto [ec, n] = co_await stream.read_some(mb); - dynbuf.commit(n); - total_read += n; - if(ec == cond::eof) - co_return {{}, total_read}; - if(ec) - co_return {ec, total_read}; - if(n == mb_size) - amount = amount / 2 + amount; - } -} - -/** Read all data from a source into a dynamic buffer. - - @par Await-effects - - Reads data from `stream` by calling `source.read` repeatedly - and appending it to `dynbuf` using prepare/commit semantics - until: - - @li either @c dynbuf.size() == @c dynbuf.max_size() , - @li or a contingency on @c stream.read occurs. - - The last, potenitally partial, read is also appended. - - The value passed in the first call to `dynbuf.prepare` is `initial_amount`. - The value is grown to 1.5 times the preceding value only after a read that - completely filled the prepared buffer; otherwise it is left unchanged for - the next call. - - - @par Await-returns - - An object of type `io_result` destructuring as `[ec, n]`. - - `n` represents the total number of bytes read, - inclusive of the last partial read. - - - Contingencies: - - @li The first contingency, other than one matching to @c cond::eof, reported from awaiting @c stream.read_some . - - - @par Await-throws - - Whatever operations on @c dynbuf throw. - - This algorithm relies on @c dynbuf.prepare(n) accepting any @c n for which - `dynbuf.size() + n <= dynbuf.max_size()`. The growable buffers - (@ref string_dynamic_buffer , @ref vector_dynamic_buffer ) honor this by - reallocating, and @ref circular_dynamic_buffer by wrapping. A fixed-capacity - buffer is permitted, but not required, to compact; one that does not can - have `capacity() < max_size() - size()` after a partial @c consume (e.g. a - reused @ref flat_dynamic_buffer ). Preparing into that gap throws - (@c std::invalid_argument or @c std::length_error ), so such a buffer must be - passed without a previously consumed prefix. Allocation failure in a growable - buffer surfaces as @c std::bad_alloc . - - - @param source The source to read from. If the lifetime of `source` ends - before the coroutine finishes, the behavior is undefined. - - @param dynbuf The dynamic buffer to append data to. If the lifetime of the - buffer sequence represented by `dynbuf` ends before the coroutine finishes, - the behavior is undefined. - - @param initial_amount Hint for the value passed to `dynbuf.prepare()` - (default 2048; a value of 0 is treated as 1). There is no precondition - relating `initial_amount` to `dynbuf.max_size()`: the requested amount is - clamped so that `dynbuf.size()` plus the request never exceeds - `dynbuf.max_size()`. Reaching `max_size()` completes the operation - successfully. - - @par Remarks - Supports _IoAwaitable cancellation_. - - @par Example - - @code - capy::task read_body(capy::ReadSource auto& source) - { - std::string body; - auto [ec, n] = co_await capy::read(source, capy::dynamic_buffer(body)); - if (ec) - throw std::system_error(ec); - return body; - } - @endcode - - @see ReadSource, DynamicBufferParam -*/ -template - requires ReadSource && DynamicBufferParam -auto -read( - S& source, - DB&& dynbuf, - std::size_t initial_amount = 2048) -> - io_task -{ - std::size_t amount = initial_amount; - std::size_t total_read = 0; - for(;;) - { - if(dynbuf.size() >= dynbuf.max_size()) - co_return {{}, total_read}; - - std::size_t const available = dynbuf.max_size() - dynbuf.size(); - auto mb = dynbuf.prepare( - detail::read_prepare_amount(amount, available)); - auto const mb_size = buffer_size(mb); - auto [ec, n] = co_await source.read(mb); - dynbuf.commit(n); - total_read += n; - if(ec == cond::eof) - co_return {{}, total_read}; - if(ec) - co_return {ec, total_read}; - if(n == mb_size) - amount = amount / 2 + amount; // 1.5x growth - } -} - } // namespace capy } // namespace boost diff --git a/include/boost/capy/read_until.hpp b/include/boost/capy/read_until.hpp deleted file mode 100644 index b5ba69e76..000000000 --- a/include/boost/capy/read_until.hpp +++ /dev/null @@ -1,419 +0,0 @@ -// -// Copyright (c) 2025 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 -// - -#ifndef BOOST_CAPY_READ_UNTIL_HPP -#define BOOST_CAPY_READ_UNTIL_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { - -namespace detail { - -// Linearize a buffer sequence into a string -inline -std::string -linearize_buffers(ConstBufferSequence auto const& data) -{ - std::string linear; - linear.reserve(buffer_size(data)); - auto const end_ = end(data); - for(auto it = begin(data); it != end_; ++it) - { - const_buffer b = *it; - linear.append( - static_cast(b.data()), - b.size()); - } - return linear; -} // LCOV_EXCL_LINE gcov brace artifact (linearize_buffers is exercised) - -// Search buffer using a MatchCondition, with single-buffer optimization -template -std::size_t -search_buffer_for_match( - ConstBufferSequence auto const& data, - M const& match, - std::size_t* hint = nullptr) -{ - // Fast path: single buffer - no linearization needed - if(buffer_length(data) == 1) - { - auto const& buf = *begin(data); - return match(std::string_view( - static_cast(buf.data()), - buf.size()), hint); - } - // Multiple buffers - linearize - return match(linearize_buffers(data), hint); -} - -// Implementation coroutine for read_until with MatchCondition -template -io_task -read_until_match_impl( - Stream& stream, - B& buffers, - M match, - std::size_t initial_amount) -{ - std::size_t amount = initial_amount; - - for(;;) - { - // Check max_size before preparing - if(buffers.size() >= buffers.max_size()) - co_return {error::not_found, 0}; - - // Prepare space, respecting max_size - std::size_t const available = buffers.max_size() - buffers.size(); - std::size_t const to_prepare = (std::min)(amount, available); - if(to_prepare == 0) - co_return {error::not_found, 0}; - - auto mb = buffers.prepare(to_prepare); - auto [ec, n] = co_await stream.read_some(mb); - buffers.commit(n); - - if(!ec) - { - auto pos = search_buffer_for_match(buffers.data(), match); - if(pos != std::string_view::npos) - co_return {{}, pos}; - } - - if(ec == cond::eof) - co_return {error::eof, buffers.size()}; - if(ec) - co_return {ec, buffers.size()}; - - // Grow buffer size for next iteration - if(n == buffer_size(mb)) - amount = amount / 2 + amount; - } -} - -template -struct read_until_awaitable -{ - Stream* stream_; - M match_; - std::size_t initial_amount_; - std::optional> immediate_; - std::optional> inner_; - - using storage_type = std::conditional_t; - storage_type buffers_storage_; - - B& buffers() noexcept - { - if constexpr(OwnsBuffer) - return buffers_storage_; - else - return *buffers_storage_; - } - - // Constructor for lvalue (pointer storage) - read_until_awaitable( - Stream& stream, - B* buffers, - M match, - std::size_t initial_amount) - requires (!OwnsBuffer) - : stream_(std::addressof(stream)) - , match_(std::move(match)) - , initial_amount_(initial_amount) - , buffers_storage_(buffers) - { - auto pos = search_buffer_for_match( - buffers_storage_->data(), match_); - if(pos != std::string_view::npos) - immediate_.emplace(io_result{{}, pos}); - } - - // Constructor for rvalue adapter (owned storage) - read_until_awaitable( - Stream& stream, - B&& buffers, - M match, - std::size_t initial_amount) - requires OwnsBuffer - : stream_(std::addressof(stream)) - , match_(std::move(match)) - , initial_amount_(initial_amount) - , buffers_storage_(std::move(buffers)) - { - auto pos = search_buffer_for_match( - buffers_storage_.data(), match_); - if(pos != std::string_view::npos) - immediate_.emplace(io_result{{}, pos}); - } - - bool - await_ready() const noexcept - { - return immediate_.has_value(); - } - - std::coroutine_handle<> - await_suspend(std::coroutine_handle<> h, io_env const* env) - { - inner_.emplace(read_until_match_impl( - *stream_, buffers(), std::move(match_), initial_amount_)); - return inner_->await_suspend(h, env); - } - - io_result - await_resume() - { - if(immediate_) - return *immediate_; - return inner_->await_resume(); - } -}; - -template -using read_until_return_t = read_until_awaitable< - Stream, - std::remove_reference_t, - M, - !std::is_lvalue_reference_v>; - -} // namespace detail - -/** Match condition that searches for a delimiter string. - - Satisfies @ref MatchCondition. Returns the position after the - delimiter when found, or `npos` otherwise. Provides an overlap - hint of `delim.size() - 1` to handle delimiters spanning reads. - - @see MatchCondition, read_until -*/ -struct match_delim -{ - /** The delimiter string to search for. - - @note The referenced characters must remain valid - for the lifetime of this object and any pending - read operation. - */ - std::string_view delim; - - /** Search for the delimiter in `data`. - - @param data The data to search. - @param hint If non-null, receives the overlap hint - on miss. - @return `0` if `delim` is empty; otherwise the position - just past the delimiter, or `npos` if not found. - */ - std::size_t - operator()( - std::string_view data, - std::size_t* hint) const noexcept - { - if(delim.empty()) - return 0; - auto pos = data.find(delim); - if(pos != std::string_view::npos) - return pos + delim.size(); - if(hint) - *hint = delim.size() > 1 ? delim.size() - 1 : 0; - return std::string_view::npos; - } -}; - -/** Asynchronously read until a match condition is satisfied. - - Reads data from `stream` and appends it to `dynbuf` via calling - `stream.read_some` zero or more times and using the prepare/commit - interface until: - - @li either @c match returns a valid position, - @li or @c dynbuf.size() == @c dynbuf.max_size() , - @li or a contingency on @c stream.read_some occurs. - - If the match condition is satisfied by data in `dynbuf.data()` upon entry, - no call to `stream.read_some` is performed. - - - @par Await-returns - - An object of type `io_result` destructuring as `[ec, n]`. - - If `!ec`, the match succeeded and `n` is the position returned by the - match condition (the number of bytes through the end of the - match, i.e. the position one past the matched delimiter). - - If `bool(ec)`, the match was not found and `n` is the number of bytes - accumulated in `dynbuf` before the contingency arose. - - - Contingencies: - - @li The first contingency, reported from awaiting @c stream.read_some . - @li @c cond::not_found -- when @c dynbuf.size() == @c dynbuf.max_size() - and the match condition is not satisfied by data in @c dynbuf.data() . - - @param stream The stream to read from. The caller retains ownership. - @param dynbuf The dynamic buffer to append data to. Must remain - valid until the operation completes. - @param match The match condition callable. Copied into the awaitable. - @param initial_amount Initial bytes to read per iteration (default - 2048). Grows by 1.5x when filled. - - - - - @par Await-throws - - Whatever operations on @c dunbuf throw. - - (Note: types modeling @c DynamicBufferParam provided by Capy throw - @c std::bad_alloc from member function - @c prepare .) - - @par Remarks - Supports _IoAwaitable cancellation_. - - @par Example - - @code - task<> read_http_header( ReadStream auto& stream ) - { - std::string header; - auto [ec, n] = co_await read_until( - stream, - string_dynamic_buffer( &header ), - []( std::string_view data, std::size_t* hint ) { - auto pos = data.find( "\r\n\r\n" ); - if( pos != std::string_view::npos ) - return pos + 4; - if( hint ) - (*hint) = 3; // partial "\r\n\r" possible - return std::string_view::npos; - } ); - if( ec ) - detail::throw_system_error( ec ); - // header contains data through "\r\n\r\n" - } - @endcode - - @see read_some, MatchCondition, DynamicBufferParam -*/ -template - requires DynamicBufferParam -detail::read_until_return_t -read_until( - Stream& stream, - B&& dynbuf, - M match, - std::size_t initial_amount = 2048) -{ - constexpr bool is_lvalue = std::is_lvalue_reference_v; - using BareB = std::remove_reference_t; - - if constexpr(is_lvalue) - return detail::read_until_awaitable( - stream, std::addressof(dynbuf), std::move(match), initial_amount); - else - return detail::read_until_awaitable( - stream, std::move(dynbuf), std::move(match), initial_amount); -} - -/** Asynchronously read until a delimiter string is found. - - Reads data from the stream until the delimiter is found. This is - a convenience overload equivalent to calling `read_until` with - `match_delim{delim}`. If the delimiter already exists in the - buffer, returns immediately without I/O. - - @li The operation completes when: - @li The delimiter string is found - @li End-of-stream is reached (`cond::eof`) - @li The buffer's `max_size()` is reached (`cond::not_found`) - @li An error occurs - @li The operation is cancelled - - @par Cancellation - Supports cancellation via `stop_token` propagated through the - IoAwaitable protocol. When cancelled, returns with `cond::canceled`. - - @param stream The stream to read from. The caller retains ownership. - @param buffers The dynamic buffer to append data to. Must remain - valid until the operation completes. - @param delim The delimiter string to search for. - @param initial_amount Initial bytes to read per iteration (default - 2048). Grows by 1.5x when filled. - - @return An awaitable that await-returns `(error_code, std::size_t)`. - On success, `n` is the number of bytes through the end of the - delimiter (i.e. the position one past the delimiter). - Compare error codes to conditions: - @li `cond::eof` - EOF before delimiter; `n` is buffer size - @li `cond::not_found` - `max_size()` reached before delimiter - @li `cond::canceled` - Operation was cancelled - - @par Example - - @code - task read_line( ReadStream auto& stream ) - { - std::string line; - auto [ec, n] = co_await read_until( - stream, string_dynamic_buffer( &line ), "\r\n" ); - if( ec == cond::eof ) - co_return line; // partial line at EOF - if( ec ) - detail::throw_system_error( ec ); - line.resize( n - 2 ); // remove "\r\n" - co_return line; - } - @endcode - - @see read_until, match_delim, DynamicBufferParam -*/ -template - requires DynamicBufferParam -detail::read_until_return_t -read_until( - Stream& stream, - B&& buffers, - std::string_view delim, - std::size_t initial_amount = 2048) -{ - return read_until( - stream, - std::forward(buffers), - match_delim{delim}, - initial_amount); -} - -} // namespace capy -} // namespace boost - -#endif diff --git a/src/buffers/circular_dynamic_buffer.cpp b/src/buffers/circular_dynamic_buffer.cpp deleted file mode 100644 index 28c945093..000000000 --- a/src/buffers/circular_dynamic_buffer.cpp +++ /dev/null @@ -1,93 +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 -// - -#include -#include - -namespace boost { -namespace capy { - -auto -circular_dynamic_buffer:: -data() const noexcept -> - const_buffers_type -{ - if(in_pos_ + in_len_ <= cap_) - return {{ - const_buffer{ base_ + in_pos_, in_len_ }, - const_buffer{ base_, 0} }}; - return {{ - const_buffer{ base_ + in_pos_, cap_ - in_pos_}, - const_buffer{ base_, in_len_- (cap_ - in_pos_)} }}; -} - -auto -circular_dynamic_buffer:: -prepare(std::size_t n) -> - mutable_buffers_type -{ - // Buffer is too small for n - if(n > cap_ - in_len_) - detail::throw_length_error(); - - out_size_ = n; - auto const pos = ( - in_pos_ + in_len_) % cap_; - if(pos + n <= cap_) - return {{ - mutable_buffer{ base_ + pos, n }, - mutable_buffer{ base_, 0 } }}; - return {{ - mutable_buffer{ base_ + pos, cap_ - pos }, - mutable_buffer{ base_, n - (cap_ - pos) } }}; -} - -void -circular_dynamic_buffer:: -commit( - std::size_t n) noexcept -{ - if(n < out_size_) - in_len_ += n; - else - in_len_ += out_size_; - out_size_ = 0; -} - -void -circular_dynamic_buffer:: -consume( - std::size_t n) noexcept -{ - if(n < in_len_) - { - in_pos_ = (in_pos_ + n) % cap_; - in_len_ -= n; - } - else - { - // preserve in_pos_ if there is - // a prepared buffer - if(out_size_ != 0) - { - in_pos_ = (in_pos_ + in_len_) % cap_; - in_len_ = 0; - } - else - { - // make prepare return a - // bigger single buffer - in_pos_ = 0; - in_len_ = 0; - } - } -} - -} // capy -} // boost diff --git a/src/cond.cpp b/src/cond.cpp index bef7dcefc..ba33c180a 100644 --- a/src/cond.cpp +++ b/src/cond.cpp @@ -32,7 +32,6 @@ message(int code) const case cond::eof: return "end of file"; case cond::canceled: return "operation canceled"; case cond::stream_truncated: return "stream truncated"; - case cond::not_found: return "not found"; case cond::timeout: return "operation timed out"; default: return "unknown"; @@ -60,9 +59,6 @@ equivalent( case cond::stream_truncated: return ec == capy::error::stream_truncated; - case cond::not_found: - return ec == capy::error::not_found; - case cond::timeout: if(ec == capy::error::timeout) return true; diff --git a/src/error.cpp b/src/error.cpp index 9c9269db9..6a5f7888d 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -33,7 +33,6 @@ message(int code) const case error::canceled: return "operation canceled"; case error::test_failure: return "test failure"; case error::stream_truncated: return "stream truncated"; - case error::not_found: return "not found"; case error::timeout: return "timeout"; default: return "unknown"; @@ -54,7 +53,6 @@ default_error_condition(int code) const noexcept case error::eof: return make_error_condition(cond::eof); case error::canceled: return std::make_error_condition(std::errc::operation_canceled); case error::stream_truncated: return make_error_condition(cond::stream_truncated); - case error::not_found: return make_error_condition(cond::not_found); case error::timeout: return std::make_error_condition(std::errc::timed_out); default: return std::error_condition(code, *this); } diff --git a/test/unit/buffers/circular_dynamic_buffer.cpp b/test/unit/buffers/circular_dynamic_buffer.cpp deleted file mode 100644 index 9657ceced..000000000 --- a/test/unit/buffers/circular_dynamic_buffer.cpp +++ /dev/null @@ -1,490 +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 - -#include - -#include "test/unit/test_dynamic_buffer.hpp" -#include "test_buffers.hpp" - -#include -#include -#include -#include - -namespace boost { -namespace capy { - -static_assert(DynamicBuffer); - -struct circular_dynamic_buffer_test -{ - void - testMembers() - { - std::string pat = test_pattern(); - - // circular_dynamic_buffer() - { - circular_dynamic_buffer cb; - BOOST_TEST_EQ(cb.size(), 0); - } - - // circular_dynamic_buffer( void*, std::size_t ) - { - circular_dynamic_buffer cb( - &pat[0], pat.size()); - BOOST_TEST_EQ(cb.size(), 0); - BOOST_TEST_EQ(cb.capacity(), pat.size()); - BOOST_TEST_EQ(cb.max_size(), pat.size()); - } - - // circular_dynamic_buffer( void*, std::size_t, std:size_t ) - { - circular_dynamic_buffer cb( - &pat[0], pat.size(), 6); - BOOST_TEST_EQ(cb.size(), 6); - BOOST_TEST_EQ( - cb.capacity(), pat.size() - 6); - BOOST_TEST_EQ(cb.max_size(), pat.size()); - BOOST_TEST_EQ( - test::make_string(cb.data()), - pat.substr(0, 6)); - } - { - BOOST_TEST_THROWS( - circular_dynamic_buffer( - &pat[0], pat.size(), 600), - std::exception); - } - - // circular_dynamic_buffer( circular_dynamic_buffer const& ) - { - circular_dynamic_buffer cb0(&pat[0], pat.size()); - circular_dynamic_buffer cb1(cb0); - BOOST_TEST_EQ(cb1.size(), cb0.size()); - BOOST_TEST_EQ(cb1.capacity(), cb0.capacity()); - BOOST_TEST_EQ(cb1.max_size(), cb0.max_size()); - } - - // operator=( circular_dynamic_buffer const& ) - { - circular_dynamic_buffer cb0(&pat[0], pat.size()); - circular_dynamic_buffer cb1; - cb1 = cb0; - BOOST_TEST_EQ(cb1.size(), cb0.size()); - BOOST_TEST_EQ(cb1.capacity(), cb0.capacity()); - BOOST_TEST_EQ(cb1.max_size(), cb0.max_size()); - } - - // prepare( std::size_t ) - { - circular_dynamic_buffer cb(&pat[0], pat.size()); - BOOST_TEST_THROWS( - cb.prepare(cb.capacity() + 1), - std::length_error); - } - - // commit( std::size_t ) - { - circular_dynamic_buffer cb(&pat[0], pat.size()); - auto n = pat.size() / 2; - cb.prepare(pat.size()); - cb.commit(n); - BOOST_TEST_EQ( - test::make_string(cb.data()), - pat.substr(0, n)); - } - } - - // Helper: total size of a 2-element buffer pair - static std::size_t - bp_total_size(std::array const& bp) noexcept - { - return bp[0].size() + bp[1].size(); - } - - static std::size_t - bp_total_size(std::array const& bp) noexcept - { - return bp[0].size() + bp[1].size(); - } - - // Helper: write a string into the buffer via prepare/commit - static void - write_string( - circular_dynamic_buffer& cb, - char const* s, - std::size_t len) - { - auto mb = cb.prepare(len); - std::size_t copied = 0; - if(mb[0].size() > 0) - { - auto n = (std::min)(mb[0].size(), len); - std::memcpy(mb[0].data(), s, n); - copied += n; - } - if(mb[1].size() > 0 && copied < len) - { - auto n = (std::min)(mb[1].size(), len - copied); - std::memcpy(mb[1].data(), s + copied, n); - copied += n; - } - cb.commit(len); - } - - // Helper: read all readable bytes into a string - static std::string - read_string(circular_dynamic_buffer const& cb) - { - auto d = cb.data(); - std::string result; - result.append( - static_cast(d[0].data()), - d[0].size()); - result.append( - static_cast(d[1].data()), - d[1].size()); - return result; - } - - void - testDataWrapped() - { - char buf[8]; - circular_dynamic_buffer cb{buf, 8}; - - write_string(cb, "ABCDEF", 6); - cb.consume(5); - BOOST_TEST(cb.size() == 1); - - write_string(cb, "GHIJK", 5); - BOOST_TEST(cb.size() == 6); - - auto d = cb.data(); - BOOST_TEST(d[0].size() == 3); - BOOST_TEST(d[1].size() == 3); - BOOST_TEST(bp_total_size(d) == 6); - - std::string s = read_string(cb); - BOOST_TEST(s == "FGHIJK"); - } - - void - testPrepareTooLargeWithExistingData() - { - char buf[16]; - circular_dynamic_buffer cb{buf, 16}; - write_string(cb, "ABCDE", 5); - BOOST_TEST_THROWS(cb.prepare(12), std::length_error); - auto mb = cb.prepare(11); - BOOST_TEST(bp_total_size(mb) == 11); - } - - void - testPrepareWrapped() - { - char buf[8]; - circular_dynamic_buffer cb{buf, 8}; - - // Partial consume keeps in_pos_ at 5 - write_string(cb, "ABCDEF", 6); - cb.consume(5); - - // pos=(5+1)%8=6, 6+5=11>8 => wraps - auto mb = cb.prepare(5); - BOOST_TEST(mb[0].size() == 2); - BOOST_TEST(mb[1].size() == 3); - BOOST_TEST(bp_total_size(mb) == 5); - } - - void - testCommitMoreThanPrepared() - { - char buf[32]; - circular_dynamic_buffer cb{buf, 32}; - cb.prepare(10); - cb.commit(100); - BOOST_TEST(cb.size() == 10); - } - - void - testCommitZero() - { - char buf[32]; - circular_dynamic_buffer cb{buf, 32}; - cb.prepare(10); - cb.commit(0); - BOOST_TEST(cb.size() == 0); - } - - void - testCommitClearsOutSize() - { - char buf[32]; - circular_dynamic_buffer cb{buf, 32}; - cb.prepare(10); - cb.commit(5); - cb.commit(5); - BOOST_TEST(cb.size() == 5); - } - - void - testConsumeMoreThanSize() - { - char buf[32]; - circular_dynamic_buffer cb{buf, 32}; - write_string(cb, "ABC", 3); - cb.consume(100); - BOOST_TEST(cb.size() == 0); - } - - void - testConsumeZero() - { - char buf[32]; - circular_dynamic_buffer cb{buf, 32}; - write_string(cb, "ABCDE", 5); - cb.consume(0); - BOOST_TEST(cb.size() == 5); - BOOST_TEST(read_string(cb) == "ABCDE"); - } - - void - testConsumeAllWithPreparedBuffer() - { - char buf[16]; - circular_dynamic_buffer cb{buf, 16}; - write_string(cb, "ABCDE", 5); - cb.prepare(5); - cb.consume(5); - BOOST_TEST(cb.size() == 0); - cb.commit(3); - BOOST_TEST(cb.size() == 3); - } - - void - testConsumeAllNoPrepareResetsPos() - { - char buf[16]; - circular_dynamic_buffer cb{buf, 16}; - - write_string(cb, "ABCDE", 5); - cb.consume(3); - cb.consume(2); - BOOST_TEST(cb.size() == 0); - - auto mb = cb.prepare(16); - BOOST_TEST(mb[0].size() == 16); - BOOST_TEST(mb[1].size() == 0); - } - - void - testWrapAroundRoundTrip() - { - char buf[8]; - circular_dynamic_buffer cb{buf, 8}; - - // Partial consume to keep in_pos_ at 6 - write_string(cb, "ABCDEFG", 7); - cb.consume(6); - - write_string(cb, "123456", 6); - BOOST_TEST(cb.size() == 7); - - auto d = cb.data(); - // in_pos_=6, in_len_=7 => wraps - BOOST_TEST(d[0].size() == 2); - BOOST_TEST(d[1].size() == 5); - BOOST_TEST(read_string(cb) == "G123456"); - - cb.consume(3); - BOOST_TEST(cb.size() == 4); - BOOST_TEST(read_string(cb) == "3456"); - } - - void - testCapacityOne() - { - char buf[1]; - circular_dynamic_buffer cb{buf, 1}; - BOOST_TEST(cb.max_size() == 1); - - write_string(cb, "X", 1); - BOOST_TEST(cb.size() == 1); - BOOST_TEST(read_string(cb) == "X"); - - cb.consume(1); - BOOST_TEST(cb.size() == 0); - - BOOST_TEST_THROWS(cb.prepare(2), std::length_error); - } - - void - testPrepareZero() - { - char buf[16]; - circular_dynamic_buffer cb{buf, 16}; - auto mb = cb.prepare(0); - BOOST_TEST(bp_total_size(mb) == 0); - cb.commit(0); - BOOST_TEST(cb.size() == 0); - } - - void - testMultipleCycles() - { - char buf[10]; - circular_dynamic_buffer cb{buf, 10}; - - for(int cycle = 0; cycle < 20; ++cycle) - { - std::string msg = "C"; - msg += std::to_string(cycle % 10); - auto len = msg.size(); - BOOST_TEST(len <= 10); - write_string(cb, msg.c_str(), len); - BOOST_TEST(read_string(cb) == msg); - cb.consume(len); - BOOST_TEST(cb.size() == 0); - } - } - - void - testFuzz() - { - constexpr std::size_t cap = 64; - char buf[cap]; - circular_dynamic_buffer cb{buf, cap}; - - std::vector model; - - std::mt19937 rng{42}; - std::uniform_int_distribution action_dist{0, 2}; - std::uniform_int_distribution byte_dist{0, 255}; - - for(int iter = 0; iter < 2000; ++iter) - { - int action = action_dist(rng); - - if(action == 0) - { - std::size_t avail = cap - model.size(); - if(avail == 0) - continue; - std::uniform_int_distribution sz_dist{1, avail}; - std::size_t n = sz_dist(rng); - - std::vector data(n); - for(auto& b : data) - b = static_cast(byte_dist(rng)); - - auto mb = cb.prepare(n); - std::size_t copied = 0; - if(mb[0].size() > 0) - { - auto chunk = (std::min)(mb[0].size(), n); - std::memcpy(mb[0].data(), data.data(), chunk); - copied += chunk; - } - if(mb[1].size() > 0 && copied < n) - { - auto chunk = (std::min)(mb[1].size(), n - copied); - std::memcpy(mb[1].data(), data.data() + copied, chunk); - copied += chunk; - } - cb.commit(n); - model.insert(model.end(), data.begin(), data.end()); - } - else if(action == 1) - { - if(model.empty()) - continue; - std::uniform_int_distribution sz_dist{1, model.size()}; - std::size_t n = sz_dist(rng); - cb.consume(n); - model.erase(model.begin(), model.begin() + static_cast(n)); - } - else - { - BOOST_TEST(cb.size() == model.size()); - auto d = cb.data(); - BOOST_TEST(bp_total_size(d) == model.size()); - - std::string actual = read_string(cb); - std::string expected(model.begin(), model.end()); - BOOST_TEST(actual == expected); - } - } - - BOOST_TEST(cb.size() == model.size()); - std::string actual = read_string(cb); - std::string expected(model.begin(), model.end()); - BOOST_TEST(actual == expected); - } - - void - testCommitPartialThenPrepare() - { - char buf[16]; - circular_dynamic_buffer cb{buf, 16}; - - cb.prepare(10); - cb.commit(4); - BOOST_TEST(cb.size() == 4); - - auto mb = cb.prepare(12); - BOOST_TEST(bp_total_size(mb) == 12); - } - - void - testGrind() - { - std::string storage(64, '\0'); - auto r = test::grind_dynamic_buffer([&] { - std::fill(storage.begin(), storage.end(), '\0'); - return circular_dynamic_buffer(&storage[0], storage.size()); - }); - BOOST_TEST(r.success); - } - - void - run() - { - testMembers(); - testGrind(); - - testDataWrapped(); - testPrepareTooLargeWithExistingData(); - testPrepareWrapped(); - testPrepareZero(); - testCommitMoreThanPrepared(); - testCommitZero(); - testCommitClearsOutSize(); - testCommitPartialThenPrepare(); - testConsumeMoreThanSize(); - testConsumeZero(); - testConsumeAllWithPreparedBuffer(); - testConsumeAllNoPrepareResetsPos(); - testWrapAroundRoundTrip(); - testCapacityOne(); - testMultipleCycles(); - testFuzz(); - } -}; - -TEST_SUITE( - circular_dynamic_buffer_test, - "boost.capy.buffers.circular_dynamic_buffer"); - -} // capy -} // boost diff --git a/test/unit/buffers/flat_dynamic_buffer.cpp b/test/unit/buffers/flat_dynamic_buffer.cpp deleted file mode 100644 index f0312efe8..000000000 --- a/test/unit/buffers/flat_dynamic_buffer.cpp +++ /dev/null @@ -1,182 +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 - -#include - -#include "test/unit/test_dynamic_buffer.hpp" -#include "test_buffers.hpp" - -namespace boost { -namespace capy { - -static_assert(DynamicBuffer); - -struct flat_dynamic_buffer_test -{ - void - testMembers() - { - std::string pat = test_pattern(); - - // flat_dynamic_buffer() - { - flat_dynamic_buffer fb; - BOOST_TEST_EQ(fb.size(), 0); - BOOST_TEST_EQ(fb.max_size(), 0); - BOOST_TEST_EQ(fb.capacity(), 0); - } - - // flat_dynamic_buffer( void*, size_t ) - { - std::string s = pat; - flat_dynamic_buffer fb(&s[0], s.size()); - BOOST_TEST_EQ(fb.size(), 0); - BOOST_TEST_EQ(fb.max_size(), s.size()); - BOOST_TEST_EQ(fb.capacity(), s.size()); - } - - // flat_dynamic_buffer( void*, size_t, size_t ) - { - std::string s = pat; - flat_dynamic_buffer fb(&s[0], s.size(), 6); - BOOST_TEST_EQ(fb.size(), 6); - BOOST_TEST_EQ(fb.max_size(), s.size()); - BOOST_TEST_EQ(fb.capacity(), s.size() - 6); - } - { - std::string s = pat; - BOOST_TEST_THROWS( - flat_dynamic_buffer(&s[0], s.size(), - s.size() + 1), - std::invalid_argument); - } - - // flat_dynamic_buffer( flat_dynamic_buffer const& ) - { - std::string s = pat; - flat_dynamic_buffer fb0(&s[0], s.size()); - flat_dynamic_buffer fb1(fb0); - BOOST_TEST_EQ(fb1.size(), fb0.size()); - BOOST_TEST_EQ(fb1.max_size(), fb0.max_size()); - BOOST_TEST_EQ(fb1.capacity(), fb0.capacity()); - } - - // operator=( flat_dynamic_buffer const& ) - { - std::string s = pat; - flat_dynamic_buffer fb0(&s[0], s.size()); - flat_dynamic_buffer fb1; - fb1 = fb0; - BOOST_TEST_EQ(fb1.size(), fb0.size()); - BOOST_TEST_EQ(fb1.max_size(), fb0.max_size()); - BOOST_TEST_EQ(fb1.capacity(), fb0.capacity()); - } - - // prepare( std::size_t ) - { - std::string s = pat; - flat_dynamic_buffer fb(&s[0], s.size()); - BOOST_TEST_THROWS( - fb.prepare(s.size() + 1), - std::invalid_argument); - } - { - std::string s = pat; - flat_dynamic_buffer fb(&s[0], s.size(), 6); - BOOST_TEST_THROWS( - fb.prepare(s.size() + 1), - std::invalid_argument); - - BOOST_TEST_EQ(fb.max_size(), s.size()); - BOOST_TEST_EQ( - fb.size() + fb.capacity(), - fb.max_size()); - } - - // commit( std::size_t ) - { - std::string s = pat; - for(std::size_t i = 0; - i <= pat.size(); ++i) - { - flat_dynamic_buffer fb( - &s[0], s.size()); - fb.prepare(s.size()); - fb.commit(i); - BOOST_TEST_EQ( - test::make_string(fb.data()), - pat.substr(0, i)); - } - } - - // consume( std::size_t ) - { - std::string s = pat; - flat_dynamic_buffer fb(&s[0], s.size(), s.size()); - BOOST_TEST_EQ( - test::make_string(fb.data()), pat); - - auto const cap = fb.capacity(); - - while( fb.size() > 0 ) - { - fb.prepare(fb.capacity()); - fb.consume(1); - - if( fb.size() > 0 ) - BOOST_TEST_EQ(fb.capacity(), cap); - } - - BOOST_TEST_EQ(fb.capacity(), s.size()); - } - { - std::string s = pat; - flat_dynamic_buffer fb(&s[0], s.size(), 6); - - auto const cap = fb.capacity(); - - BOOST_TEST_NO_THROW( - fb.prepare(fb.max_size() - fb.size())); - - fb.consume(1); - BOOST_TEST_EQ(fb.capacity(), cap); - BOOST_TEST_THROWS( - fb.prepare(cap + 1), - std::invalid_argument); - } - } - - void - testGrind() - { - std::string storage(64, '\0'); - auto r = test::grind_dynamic_buffer([&] { - std::fill(storage.begin(), storage.end(), '\0'); - return flat_dynamic_buffer(&storage[0], storage.size()); - }); - BOOST_TEST(r.success); - } - - void - run() - { - testMembers(); - testGrind(); - } -}; - -TEST_SUITE( - flat_dynamic_buffer_test, - "boost.capy.buffers.flat_dynamic_buffer"); - -} // capy -} // boost diff --git a/test/unit/buffers/string_dynamic_buffer.cpp b/test/unit/buffers/string_dynamic_buffer.cpp deleted file mode 100644 index 553946404..000000000 --- a/test/unit/buffers/string_dynamic_buffer.cpp +++ /dev/null @@ -1,203 +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 - -#include - -#include "test/unit/test_dynamic_buffer.hpp" -#include "test_buffers.hpp" - -namespace boost { -namespace capy { - -static_assert(DynamicBuffer); - -struct string_dynamic_buffer_test -{ - void - testMembers() - { - std::string s; - - // ~string_dynamic_buffer - { - s = ""; - string_dynamic_buffer b(&s); - BOOST_TEST(s.empty()); - } - - // string_dynamic_buffer (move constructor) - { - std::string s0; - { - string_dynamic_buffer b0(&s0); - string_dynamic_buffer b1(std::move(b0)); - auto n = buffer_copy( - b1.prepare(5), - make_buffer("12345", 5)); - BOOST_TEST_EQ(n, 5); - b1.commit(5); - } - BOOST_TEST_EQ(s0, "12345"); - } - { - // move transfers in_size_ - std::string s0; - { - string_dynamic_buffer b0(&s0); - buffer_copy(b0.prepare(5), make_buffer("12345", 5)); - b0.commit(5); - BOOST_TEST_EQ(b0.size(), 5); - string_dynamic_buffer b1(std::move(b0)); - BOOST_TEST_EQ(b1.size(), 5); - BOOST_TEST_EQ( - test::make_string(b1.data()), "12345"); - } - BOOST_TEST_EQ(s0, "12345"); - } - - // string_dynamic_buffer(std::string) - { - s = ""; - string_dynamic_buffer b(&s); - BOOST_TEST_EQ( - b.max_size(), s.max_size()); - } - - // string_dynamic_buffer(std::string, std::size_t) - // max_size() - { - s = ""; - string_dynamic_buffer b(&s, 20); - BOOST_TEST_EQ(b.max_size(), 20); - } - - // size() - { - s = "1234"; - string_dynamic_buffer b(&s); - BOOST_TEST_EQ(b.size(), 4); - } - - // capacity() - { - { - s = ""; - s.reserve(30); - string_dynamic_buffer b(&s); - BOOST_TEST_GE(b.capacity(), 30); - } - { - s = ""; - s.reserve(30); - string_dynamic_buffer b(&s, 10); - BOOST_TEST_GE(b.capacity(), 10); - } - } - - // data() - { - s = "1234"; - string_dynamic_buffer b(&s); - BOOST_TEST_EQ( - test::make_string(b.data()), - "1234"); - } - - // prepare() - { - { - string_dynamic_buffer b(&s, 3); - BOOST_TEST_THROWS( - b.prepare(5), - std::invalid_argument); - } - { - s = std::string(); - string_dynamic_buffer b(&s); - auto dest = b.prepare(10); - BOOST_TEST_GE(s.capacity(), - buffer_size(dest)); - } - { - s = std::string(); - string_dynamic_buffer b(&s); - b.prepare(10); - auto dest = b.prepare(10); - BOOST_TEST_EQ( - buffer_size(dest), - 10); - } - } - - // commit() - { - s = ""; - { - string_dynamic_buffer b(&s); - auto n = buffer_copy( - b.prepare(5), - make_buffer("12345", 5)); - BOOST_TEST_EQ(n, 5); - b.commit(3); - BOOST_TEST_EQ(b.size(), 3); - } - BOOST_TEST_EQ(s, "123"); - } - - // consume() - { - { - s = "12345"; - { - string_dynamic_buffer b(&s); - b.consume(2); - } - BOOST_TEST_EQ(s, "345"); - } - { - s = "12345"; - { - string_dynamic_buffer b(&s); - b.consume(5); - BOOST_TEST_EQ( - buffer_size(b.data()), 0); - } - BOOST_TEST(s.empty()); - } - } - } - - void - testGrind() - { - std::string s; - auto r = test::grind_dynamic_buffer([&] { - s.clear(); - return string_dynamic_buffer(&s); - }); - BOOST_TEST(r.success); - } - - void - run() - { - testMembers(); - testGrind(); - } -}; - -TEST_SUITE( - string_dynamic_buffer_test, - "boost.capy.buffers.string_dynamic_buffer"); - -} // capy -} // boost diff --git a/test/unit/buffers/vector_dynamic_buffer.cpp b/test/unit/buffers/vector_dynamic_buffer.cpp deleted file mode 100644 index 3a09e49e4..000000000 --- a/test/unit/buffers/vector_dynamic_buffer.cpp +++ /dev/null @@ -1,218 +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 - -#include - -#include "test/unit/test_dynamic_buffer.hpp" -#include "test_buffers.hpp" - -namespace boost { -namespace capy { - -static_assert(DynamicBuffer); - -struct vector_dynamic_buffer_test -{ - void - testMembers() - { - std::vector v; - - // ~vector_dynamic_buffer - { - v.clear(); - vector_dynamic_buffer b(&v); - BOOST_TEST(v.empty()); - } - - // max_size smaller than the vector's current size truncates it - { - std::vector v0(10, 'x'); - vector_dynamic_buffer b(&v0, 4); - BOOST_TEST_EQ(v0.size(), 4u); - BOOST_TEST_EQ(b.size(), 4u); - } - - // vector_dynamic_buffer( move constructor ) - { - std::vector v0; - { - vector_dynamic_buffer b0(&v0); - vector_dynamic_buffer b1(std::move(b0)); - auto n = buffer_copy( - b1.prepare(5), - make_buffer("12345", 5)); - BOOST_TEST_EQ(n, 5); - b1.commit(5); - } - BOOST_TEST_EQ(v0.size(), 5); - BOOST_TEST(std::equal( - v0.begin(), v0.end(), "12345")); - } - { - // move transfers in_size_ - std::vector v0; - { - vector_dynamic_buffer b0(&v0); - buffer_copy(b0.prepare(5), make_buffer("12345", 5)); - b0.commit(5); - BOOST_TEST_EQ(b0.size(), 5); - vector_dynamic_buffer b1(std::move(b0)); - BOOST_TEST_EQ(b1.size(), 5); - BOOST_TEST_EQ( - test::make_string(b1.data()), "12345"); - } - BOOST_TEST_EQ(v0.size(), 5); - } - - // vector_dynamic_buffer( vector_type* ) - { - v.clear(); - vector_dynamic_buffer b(&v); - BOOST_TEST_EQ( - b.max_size(), v.max_size()); - } - - // vector_dynamic_buffer( vector_type*, std::size_t ) - // max_size() - { - v.clear(); - vector_dynamic_buffer b(&v, 20); - BOOST_TEST_EQ(b.max_size(), 20); - } - - // size() - { - v.assign({'1', '2', '3', '4'}); - vector_dynamic_buffer b(&v); - BOOST_TEST_EQ(b.size(), 4); - } - - // capacity() - { - { - v.clear(); - v.reserve(30); - vector_dynamic_buffer b(&v); - BOOST_TEST_GE(b.capacity(), 30); - } - { - v.clear(); - v.reserve(30); - vector_dynamic_buffer b(&v, 10); - BOOST_TEST_GE(b.capacity(), 10); - } - } - - // data() - { - v.assign({'1', '2', '3', '4'}); - vector_dynamic_buffer b(&v); - BOOST_TEST_EQ( - test::make_string(b.data()), - "1234"); - } - - // prepare() - { - { - v.clear(); - vector_dynamic_buffer b(&v, 3); - BOOST_TEST_THROWS( - b.prepare(5), - std::invalid_argument); - } - { - v.clear(); - vector_dynamic_buffer b(&v); - auto dest = b.prepare(10); - BOOST_TEST_GE(v.capacity(), - buffer_size(dest)); - } - { - v.clear(); - vector_dynamic_buffer b(&v); - b.prepare(10); - auto dest = b.prepare(10); - BOOST_TEST_EQ( - buffer_size(dest), - 10); - } - } - - // commit() - { - v.clear(); - { - vector_dynamic_buffer b(&v); - auto n = buffer_copy( - b.prepare(5), - make_buffer("12345", 5)); - BOOST_TEST_EQ(n, 5); - b.commit(3); - BOOST_TEST_EQ(b.size(), 3); - } - BOOST_TEST_EQ(v.size(), 3); - BOOST_TEST(std::equal( - v.begin(), v.end(), "123")); - } - - // consume() - { - { - v.assign({'1', '2', '3', '4', '5'}); - { - vector_dynamic_buffer b(&v); - b.consume(2); - } - BOOST_TEST_EQ(v.size(), 3); - BOOST_TEST(std::equal( - v.begin(), v.end(), "345")); - } - { - v.assign({'1', '2', '3', '4', '5'}); - { - vector_dynamic_buffer b(&v); - b.consume(5); - BOOST_TEST_EQ( - buffer_size(b.data()), 0); - } - BOOST_TEST(v.empty()); - } - } - } - - void - testGrind() - { - std::vector v; - auto r = test::grind_dynamic_buffer([&] { - v.clear(); - return vector_dynamic_buffer(&v); - }); - BOOST_TEST(r.success); - } - - void - run() - { - testMembers(); - testGrind(); - } -}; - -TEST_SUITE( - vector_dynamic_buffer_test, - "boost.capy.buffers.vector_dynamic_buffer"); - -} // capy -} // boost diff --git a/test/unit/concept/dynamic_buffer.cpp b/test/unit/concept/dynamic_buffer.cpp deleted file mode 100644 index 1ee3e5fbc..000000000 --- a/test/unit/concept/dynamic_buffer.cpp +++ /dev/null @@ -1,330 +0,0 @@ -// -// Copyright (c) 2025 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 - -#include -#include -#include - -#include "test_suite.hpp" - -namespace boost { -namespace capy { - -namespace { - -//---------------------------------------------------------- -// Valid DynamicBuffer types -//---------------------------------------------------------- - -struct valid_dynamic_buffer -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Valid: single buffer types as buffer sequences -struct valid_dynamic_buffer_single -{ - using const_buffers_type = const_buffer; - using mutable_buffers_type = mutable_buffer; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Valid: with adapter tag (for DynamicBufferParam) -struct valid_dynamic_buffer_adapter -{ - using is_dynamic_buffer_adapter = void; - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -//---------------------------------------------------------- -// Invalid DynamicBuffer types -//---------------------------------------------------------- - -// Invalid: missing const_buffers_type -struct invalid_missing_const_buffers_type -{ - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffer data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: missing mutable_buffers_type -struct invalid_missing_mutable_buffers_type -{ - using const_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffer prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: missing size() -struct invalid_missing_size -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: missing max_size() -struct invalid_missing_max_size -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: missing capacity() -struct invalid_missing_capacity -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: missing data() -struct invalid_missing_data -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: missing prepare() -struct invalid_missing_prepare -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: missing commit() -struct invalid_missing_commit -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void consume(std::size_t) {} -}; - -// Invalid: missing consume() -struct invalid_missing_consume -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} -}; - -// Invalid: data() returns wrong type -struct invalid_data_wrong_type -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - int data() const { return 0; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: prepare() returns wrong type -struct invalid_prepare_wrong_type -{ - using const_buffers_type = std::span; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - int prepare(std::size_t) { return 0; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: const_buffers_type not a ConstBufferSequence -struct invalid_const_buffers_not_sequence -{ - using const_buffers_type = int; - using mutable_buffers_type = std::span; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return 0; } - mutable_buffers_type prepare(std::size_t) { return {}; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -// Invalid: mutable_buffers_type not a MutableBufferSequence -struct invalid_mutable_buffers_not_sequence -{ - using const_buffers_type = std::span; - using mutable_buffers_type = int; - - std::size_t size() const { return 0; } - std::size_t max_size() const { return 0; } - std::size_t capacity() const { return 0; } - const_buffers_type data() const { return {}; } - mutable_buffers_type prepare(std::size_t) { return 0; } - void commit(std::size_t) {} - void consume(std::size_t) {} -}; - -} // namespace - -//---------------------------------------------------------- -// Static assertions: DynamicBuffer -//---------------------------------------------------------- - -// Valid types satisfy DynamicBuffer -static_assert(DynamicBuffer); -static_assert(DynamicBuffer); -static_assert(DynamicBuffer); - -// Missing type aliases -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); - -// Missing member functions -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); - -// Wrong return types -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); - -// Buffer sequence type constraints -static_assert(!DynamicBuffer); -static_assert(!DynamicBuffer); - -//---------------------------------------------------------- -// Static assertions: DynamicBufferParam -//---------------------------------------------------------- - -// Lvalue references satisfy DynamicBufferParam -static_assert(DynamicBufferParam); -static_assert(DynamicBufferParam); - -// Rvalue adapters satisfy DynamicBufferParam -static_assert(DynamicBufferParam); -static_assert(DynamicBufferParam); - -// Rvalue non-adapters do NOT satisfy DynamicBufferParam -static_assert(!DynamicBufferParam); -static_assert(!DynamicBufferParam); - -// Invalid types don't satisfy DynamicBufferParam -static_assert(!DynamicBufferParam); -static_assert(!DynamicBufferParam); - -//---------------------------------------------------------- - -struct dynamic_buffer_test -{ - void - run() - { - } -}; - -TEST_SUITE( - dynamic_buffer_test, - "boost.capy.concept.dynamic_buffer"); - -} // namespace capy -} // namespace boost diff --git a/test/unit/cond.cpp b/test/unit/cond.cpp index e620dbca2..48d1f38e3 100644 --- a/test/unit/cond.cpp +++ b/test/unit/cond.cpp @@ -60,23 +60,6 @@ class cond_test BOOST_TEST(!(ec == cond::canceled)); } - // Message: not_found - BOOST_TEST(make_error_condition(cond::not_found).message() == "not found"); - - // Equivalence: error::not_found == cond::not_found - { - auto ec = make_error_code(error::not_found); - BOOST_TEST(ec == cond::not_found); - BOOST_TEST(!(ec == cond::eof)); - BOOST_TEST(!(ec == cond::canceled)); - } - - // Non-matching codes don't match not_found - { - auto ec = make_error_code(error::eof); - BOOST_TEST(!(ec == cond::not_found)); - } - // Remaining messages, including the default branch. auto const ecnd = make_error_condition(cond::eof); auto const& cat = ecnd.category(); diff --git a/test/unit/error.cpp b/test/unit/error.cpp index d11a98ebe..242606e65 100644 --- a/test/unit/error.cpp +++ b/test/unit/error.cpp @@ -40,8 +40,6 @@ class error_category_test BOOST_TEST( cat.message(static_cast(error::stream_truncated)) == "stream truncated"); - BOOST_TEST( - cat.message(static_cast(error::not_found)) == "not found"); BOOST_TEST(cat.message(static_cast(error::timeout)) == "timeout"); // Out-of-range value hits the default branch. diff --git a/test/unit/read.cpp b/test/unit/read.cpp index 944e3a741..cc5bd1f37 100644 --- a/test/unit/read.cpp +++ b/test/unit/read.cpp @@ -10,14 +10,10 @@ // Test that header file is self-contained. #include -#include -#include #include -#include #include #include #include -#include #include #include "test_suite.hpp" @@ -129,58 +125,6 @@ struct buffer_pair_factory } }; -//---------------------------------------------------------- -// Dynamic Buffer Factories for ReadSource tests -//---------------------------------------------------------- - -struct string_dynbuf_factory -{ - std::string str; - - string_dynamic_buffer - buffer() - { - str.clear(); - return string_dynamic_buffer(&str); - } - - std::string const& - data() const - { - return str; - } -}; - -struct circular_dynamic_buffer_factory -{ - char storage[4096]; - circular_dynamic_buffer cb; - - circular_dynamic_buffer_factory() - : cb(storage, sizeof(storage)) - { - } - - circular_dynamic_buffer& - buffer() - { - cb = circular_dynamic_buffer(storage, sizeof(storage)); - return cb; - } - - std::string - data() const - { - std::string result; - auto bufs = cb.data(); - for(auto const& buf : bufs) - result.append( - static_cast(buf.data()), - buf.size()); - return result; - } -}; - } // namespace // Mock whose read_some reports a contingency in the SAME completion that @@ -443,466 +387,12 @@ struct read_test testFullTransferContingency(); } - //---------------------------------------------------------- - // ReadSource tests (DynamicBuffer) - //---------------------------------------------------------- - - void - testSourceStringDynBuf() - { - // Read all data until EOF - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - rs.provide("hello world"); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 11u); - BOOST_TEST_EQ(df.data(), "hello world"); - })); - - // Read large data (tests growth strategy) - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - std::string large_data(10000, 'x'); - rs.provide(large_data); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 10000u); - BOOST_TEST_EQ(df.data().size(), 10000u); - BOOST_TEST(df.data() == large_data); - })); - - // Empty source - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 0u); - BOOST_TEST(df.data().empty()); - })); - - // Custom initial_amount - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - rs.provide("small"); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db, 64); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 5u); - BOOST_TEST_EQ(df.data(), "small"); - })); - } - - void - testSourceCircularBuffer() - { - // Read all data until EOF - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - rs.provide("hello world"); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 11u); - BOOST_TEST_EQ(df.data(), "hello world"); - })); - - // Read larger data - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - std::string data(1000, 'y'); - rs.provide(data); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 1000u); - BOOST_TEST_EQ(df.data().size(), 1000u); - BOOST_TEST(df.data() == data); - })); - - // Empty source - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 0u); - BOOST_TEST(df.data().empty()); - })); - - // Custom initial_amount - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_source rs(f); - rs.provide("tiny"); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db, 128); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(df.data(), "tiny"); - })); - } - - void - testReadSource() - { - testSourceStringDynBuf(); - testSourceCircularBuffer(); - } - - //---------------------------------------------------------- - // ReadStream + DynamicBuffer tests - //---------------------------------------------------------- - - void - testStreamDynBufString() - { - // Read all data until EOF - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("hello world"); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 11u); - BOOST_TEST_EQ(df.data(), "hello world"); - })); - - // Read large data (tests growth strategy) - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - std::string large_data(10000, 'x'); - rs.provide(large_data); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 10000u); - BOOST_TEST_EQ(df.data().size(), 10000u); - BOOST_TEST(df.data() == large_data); - })); - - // Empty stream (immediate EOF) - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 0u); - BOOST_TEST(df.data().empty()); - })); - - // Custom initial_amount - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("small"); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db, 64); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 5u); - BOOST_TEST_EQ(df.data(), "small"); - })); - - // Chunked reads with max_read_size - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f, 3); - rs.provide("hello world"); - - string_dynbuf_factory df; - auto db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 11u); - BOOST_TEST_EQ(df.data(), "hello world"); - })); - } - - void - testStreamDynBufCircular() - { - // Read all data until EOF - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("hello world"); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 11u); - BOOST_TEST_EQ(df.data(), "hello world"); - })); - - // Read larger data - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - std::string data(1000, 'y'); - rs.provide(data); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 1000u); - BOOST_TEST_EQ(df.data().size(), 1000u); - BOOST_TEST(df.data() == data); - })); - - // Empty stream - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 0u); - BOOST_TEST(df.data().empty()); - })); - - // Custom initial_amount - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("tiny"); - - circular_dynamic_buffer_factory df; - auto& db = df.buffer(); - auto [ec, n] = co_await read(rs, db, 128); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(df.data(), "tiny"); - })); - } - - void - testStreamDynBuf() - { - testStreamDynBufString(); - testStreamDynBufCircular(); - } - - //---------------------------------------------------------- - // Bounded dynamic buffer: reaching max_size completes the - // transfer successfully and never throws (issue #318). Before - // the fix, prepare() threw std::invalid_argument (string) or - // std::length_error (circular) when the requested amount - // exceeded the remaining capacity. - //---------------------------------------------------------- - - void - testDynBufMaxSize() - { - // These cases verify the deterministic max_size behavior, so they - // use inert() (a single fault-free run) rather than armed(). - - // ReadStream, string buffer: default initial_amount (2048) - // far exceeds max_size; data exceeds max_size. Fills to - // max_size and stops; no throw. - BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("abcdef"); - - std::string s; - string_dynamic_buffer db(&s, 4); - auto [ec, n] = co_await read(rs, db); - BOOST_TEST(! ec); - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(s, "abcd"); - })); - - // ReadStream, string buffer: explicit initial_amount > max_size. - BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("hello world"); - - std::string s; - string_dynamic_buffer db(&s, 4); - auto [ec, n] = co_await read(rs, db, 100); - BOOST_TEST(! ec); - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(s, "hell"); - })); - - // ReadStream, string buffer: EOF before max_size is reached. - // eof remains a success (n is the bytes read so far). - BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("ab\n"); - - std::string s; - string_dynamic_buffer db(&s, 4); - auto [ec, n] = co_await read(rs, db, 1); - BOOST_TEST(! ec); - BOOST_TEST_EQ(n, 3u); - BOOST_TEST_EQ(s, "ab\n"); - })); - - // ReadStream, circular buffer: data exceeds max_size. Exercises - // the std::length_error path that used to throw. - BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("abcdef"); - - char storage[4]; - circular_dynamic_buffer db(storage, sizeof(storage)); - auto [ec, n] = co_await read(rs, db); - BOOST_TEST(! ec); - BOOST_TEST_EQ(n, 4u); - std::string out; - for(auto const& b : db.data()) - out.append( - static_cast(b.data()), b.size()); - BOOST_TEST_EQ(out, "abcd"); - })); - - // ReadStream, flat buffer (fresh, no consumed prefix): fills to - // max_size and stops; no throw. - BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("abcdef"); - - char storage[4]; - flat_dynamic_buffer db(storage, sizeof(storage)); - auto [ec, n] = co_await read(rs, db); - BOOST_TEST(! ec); - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(std::string_view(storage, 4), "abcd"); - })); - - // ReadStream, flat buffer reused after a partial consume: it does not - // compact, so capacity() (0) < max_size()-size() (4) and prepare - // throws. This documents the no-compaction limitation (issue #318): - // such a buffer must be passed without a previously consumed prefix. - BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("xyz"); - - char storage[8] = {}; - // 8 bytes readable, then consume 4: in_pos_=4, size()=4, - // capacity()=0, max_size()=8. - flat_dynamic_buffer db( - storage, sizeof(storage), sizeof(storage)); - db.consume(4); - - bool threw = false; - try - { - auto r = co_await read(rs, db); - (void)r; - } - catch(std::invalid_argument const&) - { - threw = true; - } - BOOST_TEST(threw); - })); - - // ReadSource overload: same clamping applies. - BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task - { - test::read_source rs(f); - rs.provide("abcdef"); - - std::string s; - string_dynamic_buffer db(&s, 4); - auto [ec, n] = co_await read(rs, db); - BOOST_TEST(! ec); - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(s, "abcd"); - })); - } - //---------------------------------------------------------- void run() { testReadStream(); - testReadSource(); - testStreamDynBuf(); - testDynBufMaxSize(); } }; diff --git a/test/unit/read_until.cpp b/test/unit/read_until.cpp deleted file mode 100644 index 8c70212e8..000000000 --- a/test/unit/read_until.cpp +++ /dev/null @@ -1,547 +0,0 @@ -// -// Copyright (c) 2025 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 - -#include -#include -#include -#include -#include - -#include "test_suite.hpp" - -#include -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { - -struct read_until_test -{ - //---------------------------------------------------------- - // Fast path tests (delimiter already in buffer) - //---------------------------------------------------------- - - void - testFastPath() - { - // Delimiter already in buffer - no I/O needed - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - // Don't provide any data - buffer is pre-filled - - std::string data = "hello\r\nworld\r\n"; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 7u); // "hello\r\n" - })); - - // Multiple delimiters in buffer - second call also fast - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - - std::string data = "line1\nline2\nline3\n"; - string_dynamic_buffer db(&data); - - // First read_until - auto [ec1, n1] = co_await read_until(rs, db, "\n"); - if(ec1) - co_return; - BOOST_TEST_EQ(n1, 6u); // "line1\n" - - // Consume first line - db.consume(n1); - - // Second read_until - should also be fast path - auto [ec2, n2] = co_await read_until(rs, db, "\n"); - if(ec2) - co_return; - BOOST_TEST_EQ(n2, 6u); // "line2\n" - })); - - // Empty delimiter returns 0 immediately - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - - std::string data = "some data"; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), ""); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 0u); - })); - } - - //---------------------------------------------------------- - // Basic read tests - //---------------------------------------------------------- - - void - testBasicRead() - { - // Delimiter found in first read - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("hello\r\nworld"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 7u); - BOOST_TEST_EQ(data.substr(0, n), "hello\r\n"); - })); - - // Single character delimiter - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("abc:def:ghi"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), ":"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(data.substr(0, n), "abc:"); - })); - - // Delimiter at very beginning - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("\r\nhello"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 2u); - })); - - // Delimiter at exact end of data - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("hello\r\n"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 7u); - BOOST_TEST_EQ(data, "hello\r\n"); - })); - } - - //---------------------------------------------------------- - // Chunked read tests (delimiter spans reads) - //---------------------------------------------------------- - - void - testChunkedRead() - { - // Delimiter spans multiple reads - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f, 3); // max 3 bytes per read - rs.provide("hello\r\nworld"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 7u); - BOOST_TEST_EQ(data.substr(0, n), "hello\r\n"); - })); - - // Very small chunks, delimiter split across boundary - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f, 1); // 1 byte per read - rs.provide("ab\r\ncd"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 4u); - BOOST_TEST_EQ(data.substr(0, n), "ab\r\n"); - })); - - // Long delimiter spanning reads - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f, 2); - rs.provide("helloENDMARKworld"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "ENDMARK"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 12u); // "helloENDMARK" - })); - - // Lvalue (non-owning) buffer with a chunked read: the - // delimiter is not present up front, so the inner coroutine - // runs and dereferences the pointer-stored buffer. - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f, 3); // max 3 bytes per read - rs.provide("hello\r\nworld"); - - std::string data; - string_dynamic_buffer db(&data); // lvalue: pointer storage - auto [ec, n] = co_await read_until(rs, db, "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 7u); - BOOST_TEST_EQ(data.substr(0, n), "hello\r\n"); - })); - } - - //---------------------------------------------------------- - // Error condition tests - //---------------------------------------------------------- - - void - testErrorConditions() - { - // EOF before delimiter found - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("no delimiter here"); - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec && ec != cond::eof) - co_return; - BOOST_TEST(ec == cond::eof); - BOOST_TEST_EQ(n, 17u); // All data read - BOOST_TEST_EQ(data, "no delimiter here"); - })); - - // max_size reached before delimiter - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("this is a very long string without the delimiter"); - - std::string data; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data, 10), "\r\n"); // max 10 bytes - if(ec && ec != cond::not_found) - co_return; - BOOST_TEST(ec == cond::not_found); - BOOST_TEST_EQ(n, 0u); - })); - - // Delimiter not in data at all, small max_size - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("aaaaaaaaaaaaaaaaaaaa"); - - std::string data; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data, 5), "XXX"); - if(ec && ec != cond::not_found) - co_return; - BOOST_TEST(ec == cond::not_found); - BOOST_TEST_EQ(n, 0u); - })); - - // Empty stream - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - // No data provided - - std::string data; - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\n"); - if(ec && ec != cond::eof) - co_return; - BOOST_TEST(ec == cond::eof); - BOOST_TEST_EQ(n, 0u); - })); - } - - //---------------------------------------------------------- - // Pre-filled buffer tests - //---------------------------------------------------------- - - void - testPrefilledBuffer() - { - // Buffer has partial data, delimiter comes from stream - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("llo\r\nworld"); - - std::string data = "he"; // Pre-fill with "he" - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 7u); // "hello\r\n" - BOOST_TEST_EQ(data.substr(0, n), "hello\r\n"); - })); - - // Delimiter partially in buffer, completed by stream - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("\nrest"); - - std::string data = "line\r"; // Has \r but not \n - auto [ec, n] = co_await read_until(rs, dynamic_buffer(data), "\r\n"); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 6u); // "line\r\n" - })); - } - - //---------------------------------------------------------- - // MatchCondition tests - //---------------------------------------------------------- - - void - testMatchCondition() - { - // Custom matcher: find double newline (HTTP header end) - auto match_header_end = []( - std::string_view data, - std::size_t* hint) -> std::size_t - { - auto pos = data.find("\r\n\r\n"); - if(pos != std::string_view::npos) - return pos + 4; - if(hint) - *hint = 3; // Partial "\r\n\r" possible - return std::string_view::npos; - }; - - // Basic match condition usage - BOOST_TEST(test::fuse().armed([&](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("GET / HTTP/1.1\r\nHost: example.com\r\n\r\nbody"); - - std::string data; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data), match_header_end); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 37u); // Headers including \r\n\r\n - })); - - // Match condition with chunked reads - BOOST_TEST(test::fuse().armed([&](test::fuse& f) -> task - { - test::read_stream rs(f, 5); // 5 bytes at a time - rs.provide("A\r\n\r\nB"); - - std::string data; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data), match_header_end); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 5u); - })); - - // Match condition: delimiter not found (EOF) - BOOST_TEST(test::fuse().armed([&](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("partial\r\n"); - - std::string data; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data), match_header_end); - if(ec && ec != cond::eof) - co_return; - BOOST_TEST(ec == cond::eof); - })); - - // Match at position 0 (empty match returns immediately) - auto match_zero = [](std::string_view, std::size_t*) -> std::size_t - { - return 0; // Always returns position 0 - }; - - BOOST_TEST(test::fuse().armed([&](test::fuse& f) -> task - { - test::read_stream rs(f); - - std::string data = "anything"; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data), match_zero); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 0u); - })); - - // Stateful matcher: count occurrences - struct match_nth_newline - { - int target; - mutable int count = 0; - - std::size_t - operator()(std::string_view data, std::size_t* hint) const - { - count = 0; - for(std::size_t i = 0; i < data.size(); ++i) - { - if(data[i] == '\n') - { - ++count; - if(count == target) - return i + 1; - } - } - if(hint) - *hint = 0; - return std::string_view::npos; - } - }; - - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f); - rs.provide("line1\nline2\nline3\nline4\n"); - - std::string data; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data), match_nth_newline{3}); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 18u); // Up to third \n - })); - } - - void - testMoveOnlyMatcher() - { - struct move_only_matcher - { - std::unique_ptr delim; - - explicit move_only_matcher(std::string d) - : delim(std::make_unique(std::move(d))) - { - } - - std::size_t - operator()( - std::string_view data, - std::size_t* hint) const - { - auto pos = data.find(*delim); - if(pos != std::string_view::npos) - return pos + delim->size(); - if(hint) - *hint = delim->size() > 1 ? delim->size() - 1 : 0; - return std::string_view::npos; - } - }; - - static_assert(MatchCondition); - static_assert(! std::is_copy_constructible_v); - - // Chunked reads route the matcher through await_suspend, which - // moves rather than copies it. - BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task - { - test::read_stream rs(f, 4); - rs.provide("hello\r\nworld"); - - std::string data; - auto [ec, n] = co_await read_until( - rs, dynamic_buffer(data), move_only_matcher("\r\n")); - if(ec) - co_return; - - BOOST_TEST_EQ(n, 7u); // "hello\r\n" - })); - } - - //---------------------------------------------------------- - - void - testMatchHelpers() - { - // A multi-buffer sequence forces linearize_buffers and the - // multi-buffer branch of search_buffer_for_match, which the - // contiguous string_dynamic_buffer never reaches. - const_buffer bufs[2] = { - const_buffer("hel", 3), - const_buffer("lo\r\nx", 5) - }; - std::span seq(bufs, 2); - - BOOST_TEST_EQ(detail::linearize_buffers(seq), "hello\r\nx"); - - match_delim m{"\r\n"}; - BOOST_TEST_EQ(detail::search_buffer_for_match(seq, m), 7u); - - // On a miss, a multi-character delimiter records an overlap - // hint of delim.size() - 1. - std::size_t hint = 999; - BOOST_TEST(m("partial te", &hint) == std::string_view::npos); - BOOST_TEST_EQ(hint, 1u); - } - - void - run() - { - testFastPath(); - testBasicRead(); - testChunkedRead(); - testErrorConditions(); - testPrefilledBuffer(); - testMatchCondition(); - testMoveOnlyMatcher(); - testMatchHelpers(); - } -}; - -TEST_SUITE( - read_until_test, - "boost.capy.read_until"); - -} // namespace capy -} // namespace boost diff --git a/test/unit/test_dynamic_buffer.hpp b/test/unit/test_dynamic_buffer.hpp deleted file mode 100644 index 5807cf639..000000000 --- a/test/unit/test_dynamic_buffer.hpp +++ /dev/null @@ -1,95 +0,0 @@ -// -// Copyright (c) 2025 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 -// - -#ifndef BOOST_CAPY_TEST_UNIT_TEST_DYNAMIC_BUFFER_HPP -#define BOOST_CAPY_TEST_UNIT_TEST_DYNAMIC_BUFFER_HPP - -#include -#include -#include -#include -#include -#include -#include - -#include "test_suite.hpp" - -#include - -namespace boost { -namespace capy { -namespace test { - -/** Exercises DynamicBuffer with all buffer split points. - - Uses bufgrind to test prepare/commit/consume/data/size - operations at every possible split point of a small - test string. Tests I/O round-trip via read_stream and - write_stream with error injection. - - @param make_buffer_fn Factory returning a fresh dynamic buffer. - - @return The fuse result indicating success or failure. -*/ -template -fuse::result -grind_dynamic_buffer(F&& make_buffer_fn) -{ - fuse f; - return f.armed([&](fuse& f) -> task<> { - std::string const data = "abcdefgh"; - auto cb = make_buffer(data); - bufgrind bg(cb); - - while(bg) - { - auto [b1, b2] = co_await bg.next(); - 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)); - - if(buffer_size(b1) > 0) - { - 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)); - - // Write from dynamic buffer to write_stream - write_stream ws(f); - if(db.size() > 0) - { - auto [ec, n] = co_await ws.write_some(db.data()); - if(ec) - co_return; - BOOST_TEST_EQ(n, db.size()); - } - - // Verify round-trip - BOOST_TEST_EQ(ws.data(), buffer_to_string(b1)); - - db.consume(db.size()); - BOOST_TEST_EQ(db.size(), 0u); - } - }); -} - -} // test -} // capy -} // boost - -#endif