removes buffer flush and adds maximum capacity instead - resolves #185 default tip

Thu, 11 Dec 2025 17:08:17 +0100

author
Mike Becker <universe@uap-core.de>
date
Thu, 11 Dec 2025 17:08:17 +0100
changeset 1571
25ead2ffb9b5
parent 1570
8fd491bc2940

removes buffer flush and adds maximum capacity instead - resolves #185

CHANGELOG file | annotate | diff | comparison | revisions
docs/Writerside/topics/about.md file | annotate | diff | comparison | revisions
docs/Writerside/topics/buffer.h.md file | annotate | diff | comparison | revisions
src/buffer.c file | annotate | diff | comparison | revisions
src/cx/buffer.h file | annotate | diff | comparison | revisions
tests/test_buffer.c file | annotate | diff | comparison | revisions
--- a/CHANGELOG	Wed Dec 10 23:27:32 2025 +0100
+++ b/CHANGELOG	Thu Dec 11 17:08:17 2025 +0100
@@ -4,6 +4,7 @@
  * adds cx_system_page_size() to allocator.h
  * adds cxJsonFromString()
  * adds line continuation support to CxProperties / CxPropertiesConfig
+ * adds cxBufferMaximumCapacity()
  * changes cxFreeDefault() from a macro to a function so that it can be used as a simple destructor
  * changes cxBufferReserve() to allow reducing the capacity
  * changes the members of CxJson and CxJsonValue
@@ -18,6 +19,7 @@
    were not returning zero after freeing the memory when passed a size of zero
  * removes the sort_members feature from CxJsonWriter
  * removes the source and sink API from properties.h
+ * removes the flush feature from CxBuffer
 
 Version 3.2 - 2025-11-30
 ------------------------
--- a/docs/Writerside/topics/about.md	Wed Dec 10 23:27:32 2025 +0100
+++ b/docs/Writerside/topics/about.md	Thu Dec 11 17:08:17 2025 +0100
@@ -31,6 +31,7 @@
 * adds cx_system_page_size() to allocator.h
 * adds cxJsonFromString()
 * adds line continuation support to CxProperties / CxPropertiesConfig
+* adds cxBufferMaximumCapacity()
 * changes cxFreeDefault() from a macro to a function so that it can be used as a simple destructor
 * changes cxBufferReserve() to allow reducing the capacity
 * changes the members of CxJson and CxJsonValue
@@ -45,6 +46,7 @@
   were not returning zero after freeing the memory when passed a size of zero
 * removes the sort_members feature from CxJsonWriter
 * removes the source and sink API from properties.h
+* removes the flush feature from CxBuffer
 
 ### Version 3.2 - 2025-11-30 {collapsible="true"}
 
--- a/docs/Writerside/topics/buffer.h.md	Wed Dec 10 23:27:32 2025 +0100
+++ b/docs/Writerside/topics/buffer.h.md	Thu Dec 11 17:08:17 2025 +0100
@@ -6,7 +6,7 @@
 or from a file or network stream to the buffer and vice versa.
 
 More features for convenient use of the buffer can be enabled, like automatic memory management,
-automatic resizing of the buffer space, or automatic flushing of contents.
+or automatic resizing of the buffer space.
 
 The functions `cxBufferRead()` and `cxBufferWrite()` are `cx_read_func` and `cx_write_func` compatible,
 which in turn have a compatible signature to `fread()` and `fwrite()`.
@@ -122,17 +122,23 @@
 ```C
 #include <cx/buffer.h>
 
-int cxBufferReserve(CxBuffer *buffer, size_t capacity);
+int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity);
 
 int cxBufferMinimumCapacity(CxBuffer *buffer, size_t capacity);
 
+int cxBufferReserve(CxBuffer *buffer, size_t capacity);
+
 void cxBufferShrink(CxBuffer *buffer, size_t reserve);
 ```
 
+The function `cxBufferMaximumCapacity()` specifies an upper limit for auto-growing the buffer's capacity.
+If the new threshold is smaller than the current capacity, this function fails and returns non-zero.
+
 The function `cxBufferMinimumCapacity()` guarantees a buffer capacity of _at least_ `capacity`.
 It will grow the capacity in powers of two until the system's page size is reached.
 Then, the new capacity will be a multiple of the page size.
-The function returns non-zero if increasing the capacity was attempted unsuccessfully.
+The function returns non-zero if increasing the capacity was attempted unsuccessfully;
+that includes attempts to specify a minimum capacity that exceeds the maximum capacity.
 
 The function `cxBufferReserve()`, on the other hand, will reallocate the buffer's space to match exactly the requested `capacity`
 and the contents are truncated if required.
@@ -147,7 +153,7 @@
 > If the buffer is in a copy-on-write state, `cxBufferMinimumCapacity()` will perform the copy-on-write action
 > before reallocating the space.
 > The function `cxBufferShrink()` on the other hand does _nothing_ when the buffer is in a copy-on-write state,
-> because it does not make much sense to copy the memory just to have it in a smaller memory region.
+> because it makes little sense to copy the memory just to have it in a smaller memory region.
 
 ## Write
 
@@ -172,8 +178,11 @@
 
 When the capacity of the buffer is not sufficient and the `CX_BUFFER_AUTO_EXTEND` is not set in the buffer,
 items that do not fit into the buffer are discarded.
-The function always returns the actual number of items successfully written.
+The function then returns the actual number of items successfully written.
 This equals the number of bytes if and only if `size=1`.
+If `CX_BUFFER_AUTO_EXTEND` is set, the buffer is grown to it's maximum capacity
+(see `cxBufferMaximumCapacity()` in [](#capacity-management)).
+In case the allocation for auto-extension fails, the function immediately returns zero and does not write any data.
 
 The function `cxBufferPut()` is a `putc()`-like wrapper for `cxBufferWrite()` which writes the character `c`,
 converted to an `unsigned char` to the buffer.
@@ -190,8 +199,6 @@
 
 The function `cxBufferAppend()` writes the data to the end of the buffer (given by its size) regardless of the current position,
 and it also does _not_ advance the position.
-If the write operation triggered a [flush](#flushing), however, the position will be shifted left alongside the shifted buffer contents.
-In case the data at which the current position points gets flushed, the new position will be zero.
 
 ## Read
 
@@ -292,50 +299,6 @@
 which usually makes little sense on a 64-bit platform where `off_t` is already large enough to represent any reasonable offset.
 You may, however, still use those functions to express more explicitly in your code in which direction you want the contents to be shifted.
 
-## Flushing
-
-```C
-#include <cx/buffer.h>
-
-typedef struct cx_buffer_flush_config_s {
-    size_t threshold;
-    size_t blksize;
-    size_t blkmax;
-    void *target;
-    cx_write_func wfunc;
-} CxBufferFlushConfig;
-
-int cxBufferEnableFlushing(CxBuffer *buffer,
-        CxBufferFlushConfig config);
-
-size_t cxBufferFlush(CxBuffer *buffer);
-```
-
-With the function `cxBufferEnableFlushing()` you can configure a flushing strategy for the contents of the buffer.
-
-Flushing, once enabled, may happen in the following cases:
-1. when data is written to the buffer, the capacity is insufficient, and `CX_BUFFER_AUTO_EXTEND` is _not_ enabled
-2. when data is written to the buffer, and the required capacity exceeds the `threshold` configuration
-3. when `cxBufferFlush()` is called explicitly
-
-> By combining the `CX_BUFFER_AUTO_EXTEND` flag and the `threshold` setting,
-> you can create a buffer that initially starts with a small capacity, grows up to a certain threshold,
-> and starts flushing only when that threshold is exceeded.
-
-Flushing happens by invoking the `wfunc` up to `blkmax` times, writing up to `blksize` bytes from the buffer to the `target` with each call.
-The target might not accept all bytes (i.e., the `wfunc` return value indicates that fewer items have been written than requested),
-in which case the remaining data remains in the buffer.
-That means the buffer is effectively [shifted](#shift-contents) left by the number of successfully flushed bytes.
-
-> When you write large amounts of data to a buffer, multiple flush cycles might happen.
-> After the first flush operations are completed, the reclaimed space in the buffer is filled first, but if that
-> is not enough, another flush may be triggered within the same invocation of the write operation.
-> 
-> That means as much data is written to the buffer and/or flushed as possible, until neither the flush target nor the buffer accept more data.
-> {style="note"}
-
-> The function `cxBufferFlush()` simply returns zero when flushing was not enabled via `cxBufferEnableFlushing()`.
-
 <seealso>
 <category ref="apidoc">
 <a href="https://ucx.sourceforge.io/api/buffer_8h.html">buffer.h</a>
--- a/src/buffer.c	Wed Dec 10 23:27:32 2025 +0100
+++ b/src/buffer.c	Thu Dec 11 17:08:17 2025 +0100
@@ -66,21 +66,10 @@
         buffer->bytes = space;
     }
     buffer->capacity = capacity;
+    buffer->max_capacity = SIZE_MAX;
     buffer->size = 0;
     buffer->pos = 0;
 
-    buffer->flush = NULL;
-
-    return 0;
-}
-
-int cxBufferEnableFlushing(
-    CxBuffer *buffer,
-    CxBufferFlushConfig config
-) {
-    buffer->flush = cxMallocDefault(sizeof(CxBufferFlushConfig));
-    if (buffer->flush == NULL) return -1; // LCOV_EXCL_LINE
-    memcpy(buffer->flush, &config, sizeof(CxBufferFlushConfig));
     return 0;
 }
 
@@ -88,7 +77,6 @@
     if (buffer->flags & CX_BUFFER_FREE_CONTENTS) {
         cxFree(buffer->allocator, buffer->bytes);
     }
-    cxFreeDefault(buffer->flush);
     memset(buffer, 0, sizeof(CxBuffer));
 }
 
@@ -213,6 +201,9 @@
     if (newcap == buffer->capacity) {
         return 0;
     }
+    if (newcap > buffer->max_capacity) {
+        return -1;
+    }
     const int force_copy_flags = CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_COPY_ON_EXTEND;
     if (buffer->flags & force_copy_flags) {
         void *newspace = cxMalloc(buffer->allocator, newcap);
@@ -236,35 +227,46 @@
     }
 }
 
-static size_t cx_buffer_calculate_minimum_capacity(size_t mincap) {
-    unsigned long pagesize = cx_system_page_size();
-    // if page size is larger than 64 KB - for some reason - truncate to 64 KB
-    if (pagesize > 65536) pagesize = 65536;
-    if (mincap < pagesize) {
-        // when smaller as one page, map to the next power of two
-        mincap--;
-        mincap |= mincap >> 1;
-        mincap |= mincap >> 2;
-        mincap |= mincap >> 4;
-        // last operation only needed for pages larger 4096 bytes
-        // but if/else would be more expensive than just doing this
-        mincap |= mincap >> 8;
-        mincap++;
-    } else {
-        // otherwise, map to a multiple of the page size
-        mincap -= mincap % pagesize;
-        mincap += pagesize;
-        // note: if newcap is already page aligned,
-        // this gives a full additional page (which is good)
+int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity) {
+    if (capacity < buffer->capacity) {
+        return -1;
     }
-    return mincap;
+    buffer->max_capacity = capacity;
+    return 0;
 }
 
 int cxBufferMinimumCapacity(CxBuffer *buffer, size_t newcap) {
     if (newcap <= buffer->capacity) {
         return 0;
     }
-    newcap = cx_buffer_calculate_minimum_capacity(newcap);
+    if (newcap > buffer->max_capacity) {
+        return -1;
+    }
+    if (newcap < buffer->max_capacity) {
+        unsigned long pagesize = cx_system_page_size();
+        // if page size is larger than 64 KB - for some reason - truncate to 64 KB
+        if (pagesize > 65536) pagesize = 65536;
+        if (newcap < pagesize) {
+            // when smaller as one page, map to the next power of two
+            newcap--;
+            newcap |= newcap >> 1;
+            newcap |= newcap >> 2;
+            newcap |= newcap >> 4;
+            // last operation only needed for pages larger 4096 bytes
+            // but if/else would be more expensive than just doing this
+            newcap |= newcap >> 8;
+            newcap++;
+        } else {
+            // otherwise, map to a multiple of the page size
+            newcap -= newcap % pagesize;
+            newcap += pagesize;
+            // note: if newcap is already page aligned,
+            // this gives a full additional page (which is good)
+        }
+        if (newcap > buffer->max_capacity) {
+            newcap = buffer->max_capacity;
+        }
+    }
     return cxBufferReserve(buffer, newcap);
 }
 
@@ -290,54 +292,6 @@
     }
 }
 
-static size_t cx_buffer_flush_helper(
-        const CxBuffer *buffer,
-        const unsigned char *src,
-        size_t size,
-        size_t nitems
-) {
-    // flush data from an arbitrary source
-    // does not need to be the buffer's contents
-    size_t max_items = buffer->flush->blksize / size;
-    size_t fblocks = 0;
-    size_t flushed_total = 0;
-    while (nitems > 0 && fblocks < buffer->flush->blkmax) {
-        fblocks++;
-        size_t items = nitems > max_items ? max_items : nitems;
-        size_t flushed = buffer->flush->wfunc(
-            src, size, items, buffer->flush->target);
-        if (flushed > 0) {
-            flushed_total += flushed;
-            src += flushed * size;
-            nitems -= flushed;
-        } else {
-            // if no bytes can be flushed out anymore, we give up
-            break;
-        }
-    }
-    return flushed_total;
-}
-
-static size_t cx_buffer_flush_impl(CxBuffer *buffer, size_t size) {
-    // flush the current contents of the buffer
-    unsigned char *space = buffer->bytes;
-    size_t remaining = buffer->pos / size;
-    size_t flushed_total = cx_buffer_flush_helper(
-        buffer, space, size, remaining);
-
-    // shift the buffer left after flushing
-    // IMPORTANT: up to this point, copy on write must have been
-    // performed already, because we can't do error handling here
-    cxBufferShiftLeft(buffer, flushed_total*size);
-
-    return flushed_total;
-}
-
-size_t cxBufferFlush(CxBuffer *buffer) {
-    if (buffer_copy_on_write(buffer)) return 0;
-    return cx_buffer_flush_impl(buffer, 1);
-}
-
 size_t cxBufferWrite(
         const void *ptr,
         size_t size,
@@ -355,107 +309,52 @@
         return nitems;
     }
 
-    size_t len, total_flushed = 0;
-cx_buffer_write_retry:
+    size_t len;
     if (cx_szmul(size, nitems, &len)) {
         errno = EOVERFLOW;
-        return total_flushed;
+        return 0;
     }
     if (buffer->pos > SIZE_MAX - len) {
         errno = EOVERFLOW;
-        return total_flushed;
+        return 0;
     }
+    const size_t required = buffer->pos + len;
 
-    size_t required = buffer->pos + len;
-    bool perform_flush = false;
+    // check if we need to auto-extend
     if (required > buffer->capacity) {
         if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-            if (buffer->flush != NULL) {
-                size_t newcap = cx_buffer_calculate_minimum_capacity(required);
-                if (newcap > buffer->flush->threshold) {
-                    newcap = buffer->flush->threshold;
-                }
-                if (cxBufferReserve(buffer, newcap)) {
-                    return total_flushed; // LCOV_EXCL_LINE
-                }
-                if (required > newcap) {
-                    perform_flush = true;
-                }
-            } else {
-                if (cxBufferMinimumCapacity(buffer, required)) {
-                    return total_flushed; // LCOV_EXCL_LINE
-                }
-            }
-        } else {
-            if (buffer->flush != NULL) {
-                perform_flush = true;
-            } else {
-                // truncate data, if we can neither extend nor flush
-                len = buffer->capacity - buffer->pos;
-                if (size > 1) {
-                    len -= len % size;
-                }
-                nitems = len / size;
+            size_t newcap = required < buffer->max_capacity
+                    ? required : buffer->max_capacity;
+            if (cxBufferMinimumCapacity(buffer, newcap)) {
+                return 0; // LCOV_EXCL_LINE
             }
         }
     }
 
+    // check again and truncate data if capacity is still not enough
+    if (required > buffer->capacity) {
+        len = buffer->capacity - buffer->pos;
+        if (size > 1) {
+            len -= len % size;
+        }
+        nitems = len / size;
+    }
+
     // check here and not above because of possible truncation
     if (len == 0) {
-        return total_flushed;
+        return 0;
     }
 
     // check if we need to copy
     if (buffer_copy_on_write(buffer)) return 0;
 
     // perform the operation
-    if (perform_flush) {
-        size_t items_flushed;
-        if (buffer->pos == 0) {
-            // if we don't have data in the buffer, but are instructed
-            // to flush, it means that we are supposed to relay the data
-            items_flushed = cx_buffer_flush_helper(buffer, ptr, size, nitems);
-            if (items_flushed == 0) {
-                // we needed to relay data, but could not flush anything
-                // i.e. we have to give up to avoid endless trying
-                return 0;
-            }
-            nitems -= items_flushed;
-            total_flushed += items_flushed;
-            if (nitems > 0) {
-                ptr = ((unsigned char*)ptr) + items_flushed * size;
-                goto cx_buffer_write_retry;
-            }
-            return total_flushed;
-        } else {
-            items_flushed = cx_buffer_flush_impl(buffer, size);
-            if (items_flushed == 0) {
-                // flush target is full, let's try to truncate
-                size_t remaining_space;
-                if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-                    remaining_space = buffer->flush->threshold > buffer->pos
-                                          ? buffer->flush->threshold - buffer->pos
-                                          : 0;
-                } else {
-                    remaining_space = buffer->capacity > buffer->pos
-                                          ? buffer->capacity - buffer->pos
-                                          : 0;
-                }
-                nitems = remaining_space / size;
-                if (nitems == 0) {
-                    return total_flushed;
-                }
-            }
-            goto cx_buffer_write_retry;
-        }
-    } else {
-        memcpy(buffer->bytes + buffer->pos, ptr, len);
-        buffer->pos += len;
-        if (buffer->pos > buffer->size) {
-            buffer->size = buffer->pos;
-        }
-        return total_flushed + nitems;
+    memcpy(buffer->bytes + buffer->pos, ptr, len);
+    buffer->pos += len;
+    if (buffer->pos > buffer->size) {
+        buffer->size = buffer->pos;
     }
+    return nitems;
 }
 
 size_t cxBufferAppend(
--- a/src/cx/buffer.h	Wed Dec 10 23:27:32 2025 +0100
+++ b/src/cx/buffer.h	Thu Dec 11 17:08:17 2025 +0100
@@ -99,59 +99,6 @@
  */
 #define cxBufferReadFunc  ((cx_read_func) cxBufferRead)
 
-/**
- * Configuration for automatic flushing.
- */
-struct cx_buffer_flush_config_s {
-    /**
-     * The buffer may not extend beyond this threshold before starting to flush.
-     *
-     * Only used when the buffer uses #CX_BUFFER_AUTO_EXTEND.
-     * The threshold will be the maximum capacity the buffer is extended to
-     * before flushing.
-     */
-    size_t threshold;
-    /**
-     * The block size for the elements to flush.
-     */
-    size_t blksize;
-    /**
-     * The maximum number of blocks to flush in one cycle.
-     *
-     * @attention While it is guaranteed that cxBufferFlush() will not flush
-     * more blocks, this is not necessarily the case for cxBufferWrite().
-     * After performing a flush cycle, cxBufferWrite() will retry the write
-     * operation and potentially trigger another flush cycle, until the
-     * flush target accepts no more data.
-     */
-    size_t blkmax;
-
-    /**
-     * The target for the write function.
-     */
-    void *target;
-
-    /**
-     * The write-function used for flushing.
-     * If NULL, the flushed content gets discarded.
-     */
-    cx_write_func wfunc;
-};
-
-/**
- * Type alias for the flush configuration struct.
- *
- * @code
- * struct cx_buffer_flush_config_s {
- *     size_t threshold;
- *     size_t blksize;
- *     size_t blkmax;
- *     void *target;
- *     cx_write_func wfunc;
- * };
- * @endcode
- */
-typedef struct cx_buffer_flush_config_s CxBufferFlushConfig;
 
 /** Structure for the UCX buffer data. */
 struct cx_buffer_s {
@@ -168,16 +115,12 @@
     };
     /** The allocator to use for automatic memory management. */
     const CxAllocator *allocator;
-    /**
-     * Optional flush configuration
-     *
-     * @see cxBufferEnableFlushing()
-     */
-    CxBufferFlushConfig *flush;
     /** Current position of the buffer. */
     size_t pos;
     /** Current capacity (i.e. maximum size) of the buffer. */
     size_t capacity;
+    /** Maximum capacity that this buffer may grow to. */
+    size_t max_capacity;
     /** Current size of the buffer content. */
     size_t size;
     /**
@@ -231,23 +174,6 @@
         const CxAllocator *allocator, int flags);
 
 /**
- * Configures the buffer for flushing.
- *
- * Flushing can happen automatically when data is written
- * to the buffer (see cxBufferWrite()) or manually when
- * cxBufferFlush() is called.
- *
- * @param buffer the buffer
- * @param config the flush configuration
- * @retval zero success
- * @retval non-zero failure
- * @see cxBufferFlush()
- * @see cxBufferWrite()
- */
-cx_attr_nonnull
-CX_EXPORT int cxBufferEnableFlushing(CxBuffer *buffer, CxBufferFlushConfig config);
-
-/**
  * Destroys the buffer contents.
  *
  * Has no effect if the #CX_BUFFER_FREE_CONTENTS feature is not enabled.
@@ -461,6 +387,25 @@
 CX_EXPORT int cxBufferReserve(CxBuffer *buffer, size_t capacity);
 
 /**
+ * Limits the buffer's capacity.
+ *
+ * If the current capacity is already larger, this function fails and returns
+ * non-zero.
+ *
+ * The capacity limit will affect auto-extension features, as well as future
+ * calls to cxBufferMinimumCapacity() and cxBufferReserve().
+ *
+ * @param buffer the buffer
+ * @param capacity the maximum allowed capacity for this buffer
+ * @retval zero the limit is applied
+ * @retval non-zero the new limit is smaller than the current capacity
+ * @see cxBufferReserve()
+ * @see cxBufferMinimumCapacity()
+ */
+cx_attr_nonnull
+CX_EXPORT int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity);
+
+/**
  * Ensures that the buffer has a minimum capacity.
  *
  * If the current capacity is not sufficient, the buffer will be generously
@@ -473,6 +418,7 @@
  * @param capacity the minimum required capacity for this buffer
  * @retval zero the capacity was already sufficient or successfully increased
  * @retval non-zero on allocation failure
+ * @see cxBufferMaximumCapacity()
  * @see cxBufferReserve()
  * @see cxBufferShrink()
  */
@@ -502,33 +448,13 @@
 /**
  * Writes data to a CxBuffer.
  *
- * If automatic flushing is not enabled, the data is simply written into the
- * buffer at the current position, and the position of the buffer is increased
- * by the number of bytes written.
- *
- * If flushing is enabled and the buffer needs to flush, the data is flushed to
- * the target until the target signals that it cannot take more data by
- * returning zero via the respective write function. In that case, the remaining
- * data in this buffer is shifted to the beginning of this buffer so that the
- * newly available space can be used to append as much data as possible.
- *
- * This function only stops writing more elements when the flush target and this
- * buffer are both incapable of taking more data or all data has been written.
+ * If auto-extension is enabled, the buffer's capacity is automatically
+ * increased when it is not large enough to hold all data.
+ * By default, the capacity grows indefinitely, unless limited with
+ * cxBufferMaximumCapacity().
+ * When auto-extension fails, this function writes no data and returns zero.
  *
- * If, after flushing, the number of items that shall be written still exceeds
- * the capacity or flush threshold, this function tries to write all items directly
- * to the flush target, if possible.
- *
- * The number returned by this function is the number of elements from
- * @c ptr that could be written to either the flush target or the buffer.
- * That means it does @em not include the number of items that were already in
- * the buffer and were also flushed during the process.
- *
- * @attention
- * When @p size is larger than one and the contents of the buffer are not aligned
- * with @p size, flushing stops after all complete items have been flushed, leaving
- * the misaligned part in the buffer.
- * Afterward, this function only writes as many items as possible to the buffer.
+ * The position of the buffer is moved alongside the written data.
  *
  * @note The signature is compatible with the fwrite() family of functions.
  *
@@ -568,62 +494,6 @@
         size_t nitems, CxBuffer *buffer);
 
 /**
- * Performs a single flush-run on the specified buffer.
- *
- * Does nothing when the position in the buffer is zero.
- * Otherwise, the data until the current position minus
- * one is considered for flushing.
- * Note carefully that flushing will never exceed the
- * current @em position, even when the size of the
- * buffer is larger than the current position.
- *
- * One flush run will try to flush @c blkmax many
- * blocks of size @c blksize until either the @p buffer
- * has no more data to flush or the write function
- * used for flushing returns zero.
- *
- * The buffer is shifted left for that many bytes
- * the flush operation has successfully flushed.
- *
- * @par Example 1
- * Assume you have a buffer with size 340 and you are
- * at position 200. The flush configuration is
- * @c blkmax=4 and @c blksize=64 .
- * Assume that the entire flush operation is successful.
- * All 200 bytes on the left-hand-side from the current
- * position are written.
- * That means the size of the buffer is now 140 and the
- * position is zero.
- *
- * @par Example 2
- * Same as Example 1, but now the @c blkmax is 1.
- * The size of the buffer is now 276, and the position is 136.
- *
- * @par Example 3
- * Same as Example 1, but now assume the flush target
- * only accepts 100 bytes before returning zero.
- * That means the flush operation manages to flush
- * one complete block and one partial block, ending
- * up with a buffer with size 240 and position 100.
- *
- * @remark Just returns zero when flushing was not enabled with
- * cxBufferEnableFlushing().
- *
- * @remark When the buffer uses copy-on-write, the memory
- * is copied first, before attempting any flush.
- * This is, however, considered an erroneous use of the
- * buffer because it makes little sense to put
- * readonly data into an UCX buffer for flushing instead
- * of writing it directly to the target.
- *
- * @param buffer the buffer
- * @return the number of successfully flushed bytes
- * @see cxBufferEnableFlushing()
- */
-cx_attr_nonnull
-CX_EXPORT size_t cxBufferFlush(CxBuffer *buffer);
-
-/**
  * Reads data from a CxBuffer.
  *
  * The position of the buffer is increased by the number of bytes read.
@@ -647,8 +517,9 @@
  *
  * The least significant byte of the argument is written to the buffer. If the
  * end of the buffer is reached and #CX_BUFFER_AUTO_EXTEND feature is enabled,
- * the buffer capacity is extended by cxBufferMinimumCapacity(). If the feature
- * is disabled or the buffer extension fails, @c EOF is returned.
+ * the buffer capacity is extended, unless a limit set by
+ * cxBufferMaximumCapacity() is reached.
+ * If the feature is disabled or the buffer extension fails, @c EOF is returned.
  *
  * On successful writing, the position of the buffer is increased.
  *
@@ -657,8 +528,8 @@
  *
  * @param buffer the buffer to write to
  * @param c the character to write
- * @return the byte that has been written or @c EOF when the end of the stream is
- * reached, and automatic extension is not enabled or not possible
+ * @return the byte that has been written or @c EOF when the end of the
+ * stream is reached, and automatic extension is not enabled or not possible
  * @see cxBufferTerminate()
  */
 cx_attr_nonnull
@@ -667,7 +538,8 @@
 /**
  * Writes a terminating zero to a buffer at the current position.
  *
- * If successful, sets the size to the current position and advances the position by one.
+ * If successful, sets the size to the current position and advances
+ * the position by one.
  *
  * The purpose of this function is to have the written data ready to be used as
  * a C string with the buffer's size being the length of that string.
--- a/tests/test_buffer.c	Wed Dec 10 23:27:32 2025 +0100
+++ b/tests/test_buffer.c	Thu Dec 11 17:08:17 2025 +0100
@@ -40,13 +40,13 @@
         CxBuffer buf;
         void *space = cxMalloc(alloc, 16);
         cxBufferInit(&buf, space, 16, alloc, CX_BUFFER_DEFAULT);
-        CX_TEST_ASSERT(buf.flush == NULL);
         CX_TEST_ASSERT(buf.space == space);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_AUTO_EXTEND) == 0);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_FREE_CONTENTS) == 0);
         CX_TEST_ASSERT(buf.pos == 0);
         CX_TEST_ASSERT(buf.size == 0);
         CX_TEST_ASSERT(buf.capacity == 16);
+        CX_TEST_ASSERT(buf.max_capacity == SIZE_MAX);
         CX_TEST_ASSERT(buf.allocator == alloc);
         cxBufferDestroy(&buf);
         CX_TEST_ASSERT(!cx_testing_allocator_verify(&talloc));
@@ -64,13 +64,13 @@
         CxBuffer buf;
         void *space = cxMalloc(alloc, 16);
         cxBufferInit(&buf, space, 16, alloc, CX_BUFFER_AUTO_EXTEND);
-        CX_TEST_ASSERT(buf.flush == NULL);
         CX_TEST_ASSERT(buf.space == space);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_AUTO_EXTEND) == CX_BUFFER_AUTO_EXTEND);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_FREE_CONTENTS) == 0);
         CX_TEST_ASSERT(buf.pos == 0);
         CX_TEST_ASSERT(buf.size == 0);
         CX_TEST_ASSERT(buf.capacity == 16);
+        CX_TEST_ASSERT(buf.max_capacity == SIZE_MAX);
         CX_TEST_ASSERT(buf.allocator == alloc);
         cxBufferDestroy(&buf);
         CX_TEST_ASSERT(!cx_testing_allocator_verify(&talloc));
@@ -88,13 +88,13 @@
         CxBuffer buf;
         void *space = cxMalloc(alloc, 16);
         cxBufferInit(&buf, space, 16, alloc, CX_BUFFER_FREE_CONTENTS);
-        CX_TEST_ASSERT(buf.flush == NULL);
         CX_TEST_ASSERT(buf.space == space);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_AUTO_EXTEND) == 0);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS);
         CX_TEST_ASSERT(buf.pos == 0);
         CX_TEST_ASSERT(buf.size == 0);
         CX_TEST_ASSERT(buf.capacity == 16);
+        CX_TEST_ASSERT(buf.max_capacity == SIZE_MAX);
         CX_TEST_ASSERT(buf.allocator == alloc);
         CX_TEST_ASSERT(!cx_testing_allocator_verify(&talloc));
         cxBufferDestroy(&buf);
@@ -110,13 +110,13 @@
     CX_TEST_DO {
         CxBuffer buf;
         cxBufferInit(&buf, NULL, 8, alloc, CX_BUFFER_DEFAULT);
-        CX_TEST_ASSERT(buf.flush == NULL);
         CX_TEST_ASSERT(buf.space != NULL);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_AUTO_EXTEND) == 0);
         CX_TEST_ASSERT((buf.flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS);
         CX_TEST_ASSERT(buf.pos == 0);
         CX_TEST_ASSERT(buf.size == 0);
         CX_TEST_ASSERT(buf.capacity == 8);
+        CX_TEST_ASSERT(buf.max_capacity == SIZE_MAX);
         CX_TEST_ASSERT(buf.allocator == alloc);
         CX_TEST_ASSERT(!cx_testing_allocator_verify(&talloc)); // space is still allocated
         cxBufferDestroy(&buf);
@@ -134,13 +134,13 @@
         void *space = cxMalloc(alloc, 16);
         buf = cxBufferCreate(space, 16, alloc, CX_BUFFER_FREE_CONTENTS);
         CX_TEST_ASSERT(buf != NULL);
-        CX_TEST_ASSERT(buf->flush == NULL);
         CX_TEST_ASSERT(buf->space == space);
         CX_TEST_ASSERT((buf->flags & CX_BUFFER_AUTO_EXTEND) == 0);
         CX_TEST_ASSERT((buf->flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS);
         CX_TEST_ASSERT(buf->pos == 0);
         CX_TEST_ASSERT(buf->size == 0);
         CX_TEST_ASSERT(buf->capacity == 16);
+        CX_TEST_ASSERT(buf->max_capacity == SIZE_MAX);
         CX_TEST_ASSERT(buf->allocator == alloc);
         cxBufferFree(buf);
         CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
@@ -153,13 +153,13 @@
         CxBuffer *buf;
         buf = cxBufferCreate(NULL, 16, NULL, 0);
         CX_TEST_ASSERT(buf != NULL);
-        CX_TEST_ASSERT(buf->flush == NULL);
         CX_TEST_ASSERT(buf->space != NULL);
         CX_TEST_ASSERT((buf->flags & CX_BUFFER_AUTO_EXTEND) == 0);
         CX_TEST_ASSERT((buf->flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS);
         CX_TEST_ASSERT(buf->pos == 0);
         CX_TEST_ASSERT(buf->size == 0);
         CX_TEST_ASSERT(buf->capacity == 16);
+        CX_TEST_ASSERT(buf->max_capacity == SIZE_MAX);
         CX_TEST_ASSERT(buf->allocator == cxDefaultAllocator);
         cxBufferFree(buf);
     }
@@ -214,6 +214,44 @@
     cx_testing_allocator_destroy(&talloc);
 }
 
+CX_TEST(test_buffer_maximum_capacity) {
+    CxBuffer buf;
+    cxBufferInit(&buf, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND);
+    CX_TEST_DO {
+        // set maximum capacity
+        CX_TEST_ASSERT(0 == cxBufferMaximumCapacity(&buf, 512));
+        CX_TEST_ASSERT(buf.capacity == 256);
+        CX_TEST_ASSERT(buf.max_capacity == 512);
+        // increase maximum capacity again
+        CX_TEST_ASSERT(0 == cxBufferMaximumCapacity(&buf, 1024));
+        CX_TEST_ASSERT(buf.capacity == 256);
+        CX_TEST_ASSERT(buf.max_capacity == 1024);
+        // try to reduce maximum capacity below current capacity
+        CX_TEST_ASSERT(0 != cxBufferMaximumCapacity(&buf, 128));
+        CX_TEST_ASSERT(buf.capacity == 256);
+        CX_TEST_ASSERT(buf.max_capacity == 1024);
+
+        // try to reserve more than the maximum
+        CX_TEST_ASSERT(0 == cxBufferMaximumCapacity(&buf, 512));
+        CX_TEST_ASSERT(0 != cxBufferReserve(&buf, 600));
+        CX_TEST_ASSERT(buf.capacity == 256);
+        CX_TEST_ASSERT(buf.max_capacity == 512);
+        CX_TEST_ASSERT(0 == cxBufferReserve(&buf, 512));
+        CX_TEST_ASSERT(buf.capacity == 512);
+        CX_TEST_ASSERT(buf.max_capacity == 512);
+
+        // make sure that cxBufferMinimumCapacity() is capped at the limit
+        CX_TEST_ASSERT(0 == cxBufferMaximumCapacity(&buf, 777));
+        CX_TEST_ASSERT(0 != cxBufferMinimumCapacity(&buf, 800));
+        CX_TEST_ASSERT(buf.capacity == 512);
+        CX_TEST_ASSERT(buf.max_capacity == 777);
+        CX_TEST_ASSERT(0 == cxBufferMinimumCapacity(&buf, 700));
+        CX_TEST_ASSERT(buf.capacity == 777);
+        CX_TEST_ASSERT(buf.max_capacity == 777);
+    }
+    cxBufferDestroy(&buf);
+}
+
 CX_TEST(test_buffer_shrink) {
     CxTestingAllocator talloc;
     cx_testing_allocator_init(&talloc);
@@ -764,15 +802,6 @@
     }
 }
 
-static size_t mock_write_limited_rate(
-        const void *ptr,
-        size_t size,
-        cx_attr_unused size_t nitems,
-        CxBuffer *buffer
-) {
-    return cxBufferWrite(ptr, size, nitems > 2 ? 2 : nitems, buffer);
-}
-
 CX_TEST(test_buffer_write_size_one_fit) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -928,47 +957,6 @@
     cxBufferDestroy(&buf);
 }
 
-CX_TEST(test_buffer_append_flush) {
-    CxBuffer buf, target;
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    memcpy(buf.space, "prepXXXX", 8);
-    buf.capacity = 8;
-    buf.size = 6;
-    buf.pos = 4;
-    CxBufferFlushConfig flush;
-    flush.threshold = 0;
-    flush.blksize = 4;
-    flush.blkmax = 1;
-    flush.target = &target;
-    flush.wfunc = cxBufferWriteFunc;
-    cxBufferEnableFlushing(&buf, flush);
-    CX_TEST_DO{
-        size_t written = cxBufferAppend("testing", 1, 7, &buf);
-        CX_TEST_ASSERT(written == 7);
-        CX_TEST_ASSERT(buf.size == 7);
-        CX_TEST_ASSERTM(buf.pos == 0, "position not correctly reset");
-        CX_TEST_ASSERT(buf.capacity == 8);
-        CX_TEST_ASSERT(target.size == 6);
-        CX_TEST_ASSERT(target.pos == 6);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "testing", 7));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepXX", 6));
-        // second test - position only shifted by one block
-        buf.pos = 6;
-        written = cxBufferAppend("foo", 1, 3, &buf);
-        CX_TEST_ASSERT(written == 3);
-        CX_TEST_ASSERT(buf.size == 6);
-        CX_TEST_ASSERTM(buf.pos == 2, "position not correctly adjusted");
-        CX_TEST_ASSERT(buf.capacity == 8);
-        CX_TEST_ASSERT(target.size == 10);
-        CX_TEST_ASSERT(target.pos == 10);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "ingfoo", 6));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepXXtest", 10));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
 CX_TEST(test_buffer_put_fit) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -1230,6 +1218,36 @@
     cxBufferDestroy(&buf);
 }
 
+CX_TEST(test_buffer_write_maximum_capacity_exceeded) {
+    CxBuffer buf;
+    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
+    CX_TEST_DO {
+        cxBufferMaximumCapacity(&buf, 30);
+        size_t written = cxBufferPutString(&buf, "Hello, World!\nHello, Tester!");
+        CX_TEST_ASSERT(written == 28);
+        CX_TEST_ASSERT(buf.capacity == 30); // would be 32 without limit!
+        CX_TEST_ASSERT(buf.pos == 28);
+        CX_TEST_ASSERT(buf.size == 28);
+        cxBufferMaximumCapacity(&buf, 34);
+        written = cxBufferPutString(&buf, "blubberbla");
+        CX_TEST_ASSERT(written == 6);
+        CX_TEST_ASSERT(buf.capacity == 34);
+        CX_TEST_ASSERT(buf.pos == 34);
+        CX_TEST_ASSERT(buf.size == 34);
+        CX_TEST_ASSERT(0 == memcmp(buf.space, "Hello, World!\nHello, Tester!blubbe", 34));
+
+        // test multi-byte as well
+        cxBufferMaximumCapacity(&buf, 44);
+        written = cxBufferWrite("1234abcdABCD", 4, 3, &buf);
+        CX_TEST_ASSERT(written == 2);
+        CX_TEST_ASSERT(buf.capacity == 44);
+        CX_TEST_ASSERT(buf.pos == 42);
+        CX_TEST_ASSERT(buf.size == 42);
+        CX_TEST_ASSERT(0 == memcmp(buf.space, "Hello, World!\nHello, Tester!blubbe1234abcd", 42));
+    }
+    cxBufferDestroy(&buf);
+}
+
 CX_TEST(test_buffer_write_only_overwrite) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -1249,373 +1267,6 @@
     cxBufferDestroy(&buf);
 }
 
-CX_TEST(test_buffer_write_flush_at_capacity) {
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    memset(buf.space, 0, 8);
-    cxBufferPutString(&buf, "prep");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 0;
-        flush.blksize = 32;
-        flush.blkmax = 1;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        size_t written = cxBufferWrite("foo", 1, 3, &buf);
-        CX_TEST_ASSERT(written == 3);
-        CX_TEST_ASSERT(buf.pos == 7);
-        CX_TEST_ASSERT(buf.size == 7);
-        CX_TEST_ASSERT(target.pos == 0);
-        CX_TEST_ASSERT(target.size == 0);
-        written = cxBufferWrite("hello", 1, 5, &buf);
-        CX_TEST_ASSERT(written == 5);
-        CX_TEST_ASSERT(buf.pos == 5);
-        CX_TEST_ASSERT(buf.size == 5);
-        CX_TEST_ASSERT(buf.capacity == 8);
-        CX_TEST_ASSERT(target.pos == 7);
-        CX_TEST_ASSERT(target.size == 7);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "hello", 5));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoo", 7));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_flush_at_threshold) {
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferPutString(&buf, "prep");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 16;
-        flush.blksize = 32;
-        flush.blkmax = 1;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        size_t written = cxBufferWrite("foobar", 1, 6, &buf);
-        CX_TEST_ASSERT(written == 6);
-        CX_TEST_ASSERT(buf.pos == 10);
-        CX_TEST_ASSERT(buf.size == 10);
-        CX_TEST_ASSERT(buf.capacity == 16);
-        CX_TEST_ASSERT(target.pos == 0);
-        CX_TEST_ASSERT(target.size == 0);
-        written = cxBufferWrite("hello world", 1, 11, &buf);
-        CX_TEST_ASSERT(written == 11);
-        CX_TEST_ASSERT(buf.pos == 11);
-        CX_TEST_ASSERT(buf.size == 11);
-        CX_TEST_ASSERT(buf.capacity == 16);
-        CX_TEST_ASSERT(target.pos == 10);
-        CX_TEST_ASSERT(target.size == 10);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "hello", 5));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoobar", 10));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_flush_rate_limited_and_buffer_too_small) {
-    // the idea is that the target only accepts two bytes and
-    // then gives up... accepts another two bytes, gives up, etc.
-    // and at the same time, the written string is too large for
-    // the buffer (buffer can take 8, we want to write 13)
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferPutString(&buf, "prep");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 0;
-        flush.blksize = 32;
-        flush.blkmax = 1;
-        flush.target = &target;
-        flush.wfunc = (cx_write_func)mock_write_limited_rate;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        size_t written = cxBufferWrite("foo", 1, 3, &buf);
-        CX_TEST_ASSERT(written == 3);
-        CX_TEST_ASSERT(buf.pos == 7);
-        CX_TEST_ASSERT(buf.size == 7);
-        CX_TEST_ASSERT(target.pos == 0);
-        CX_TEST_ASSERT(target.size == 0);
-        written = cxBufferWrite("hello, world!", 1, 13, &buf);
-        // " world!" fits into this buffer, the remaining stuff is flushed out
-        CX_TEST_ASSERT(written == 13);
-        CX_TEST_ASSERT(buf.pos == 7);
-        CX_TEST_ASSERT(buf.size == 7);
-        CX_TEST_ASSERT(buf.capacity == 8);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, " world!", 7));
-        CX_TEST_ASSERT(target.pos == 13);
-        CX_TEST_ASSERT(target.size == 13);
-        CX_TEST_ASSERT(target.capacity >= 13);
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoohello,", 13));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_flush_multibyte) {
-    // this test case tests that flushing works correctly even when the
-    // contents in the buffer are currently not aligned with the item size
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    memset(buf.space, 0, 8);
-    cxBufferPutString(&buf, "pre");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 0;
-        flush.blksize = 4; // test blksize that is not aligned
-        flush.blkmax = 2; // test with two blocks
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        // first case: string fits after flush
-        size_t written = cxBufferWrite("foobar", 3, 2, &buf);
-        CX_TEST_ASSERT(written == 2);
-        CX_TEST_ASSERT(buf.pos == 6);
-        CX_TEST_ASSERT(buf.size == 6);
-        CX_TEST_ASSERT(target.pos == 3);
-        CX_TEST_ASSERT(target.size == 3);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "foobar", 6));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "pre", 3));
-        // second case: string does not fit, data is relayed, but only two blocks!
-        written = cxBufferWrite("bazfooBAR", 3, 3, &buf);
-        CX_TEST_ASSERT(written == 3);
-        CX_TEST_ASSERT(buf.pos == 3);
-        CX_TEST_ASSERT(buf.size == 3);
-        CX_TEST_ASSERT(target.pos == 15);
-        CX_TEST_ASSERT(target.size == 15);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "BAR", 3));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prefoobarbazfoo", 15));
-        // third case: everything can be relayed, block size is large enough
-        buf.flush->blkmax = 3;
-        written = cxBufferWrite("abcdef012", 3, 3, &buf);
-        CX_TEST_ASSERT(written == 3);
-        CX_TEST_ASSERT(buf.pos == 0);
-        CX_TEST_ASSERT(buf.size == 0);
-        CX_TEST_ASSERT(target.pos == 27);
-        CX_TEST_ASSERT(target.size == 27);
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prefoobarbazfooBARabcdef012", 27));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_flush_misaligned) {
-    // this test case tests that flushing works correctly even when the
-    // contents in the buffer are currently not aligned with the item size
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferPutString(&buf, "prep");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 0;
-        flush.blksize = 32;
-        flush.blkmax = 1;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        // first case: string fits after flush
-        size_t written = cxBufferWrite("foobar", 3, 2, &buf);
-        CX_TEST_ASSERT(written == 2);
-        CX_TEST_ASSERT(buf.pos == 7);
-        CX_TEST_ASSERT(buf.size == 7);
-        CX_TEST_ASSERT(target.pos == 3);
-        CX_TEST_ASSERT(target.size == 3);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "pfoobar", 7));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "pre", 3));
-        // second case: string does not fit, relaying not possible due to misalignment
-        // string will be truncated (two items fit into buffer, the third does not)
-        written = cxBufferWrite("bazfoobar", 3, 3, &buf);
-        CX_TEST_ASSERT(written == 2);
-        CX_TEST_ASSERT(buf.pos == 7);
-        CX_TEST_ASSERT(buf.size == 7);
-        CX_TEST_ASSERT(target.pos == 9);
-        CX_TEST_ASSERT(target.size == 9);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "rbazfoo", 7));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfooba", 9));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_flush_target_full) {
-    CxBuffer buf, target;
-    // target does NOT auto-extend and can get completely full
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferPutString(&buf, "prep");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 0;
-        flush.blksize = 32;
-        flush.blkmax = 1;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        // step one - flush 4 existing bytes, write 6 new bytes
-        size_t written = cxBufferWrite("foobar", 1, 6, &buf);
-        CX_TEST_ASSERT(written == 6);
-        CX_TEST_ASSERT(buf.pos == 6);
-        CX_TEST_ASSERT(buf.size == 6);
-        CX_TEST_ASSERT(target.pos == 4);
-        CX_TEST_ASSERT(target.size == 4);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "foobar", 6));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prep", 4));
-        // step two - can only flush 4 more bytes, but rest fits into buffer
-        written = cxBufferWrite("xyz", 1, 3, &buf);
-        CX_TEST_ASSERT(written == 3);
-        CX_TEST_ASSERT(buf.pos == 5);
-        CX_TEST_ASSERT(buf.size == 5);
-        CX_TEST_ASSERT(target.pos == 8);
-        CX_TEST_ASSERT(target.size == 8);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "arxyz", 5));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoob", 8));
-        // step three - cannot flush more, but can write 3 more bytes
-        written = cxBufferWrite("123456", 1, 6, &buf);
-        CX_TEST_ASSERT(written == 3);
-        CX_TEST_ASSERT(buf.pos == 8);
-        CX_TEST_ASSERT(buf.size == 8);
-        CX_TEST_ASSERT(target.pos == 8);
-        CX_TEST_ASSERT(target.size == 8);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "arxyz123", 8));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoob", 8));
-        // final test - cannot write anything more
-        written = cxBufferWrite("baz", 1, 3, &buf);
-        CX_TEST_ASSERT(written == 0);
-        CX_TEST_ASSERT(buf.pos == 8);
-        CX_TEST_ASSERT(buf.size == 8);
-        CX_TEST_ASSERT(target.pos == 8);
-        CX_TEST_ASSERT(target.size == 8);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "arxyz123", 8));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoob", 8));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_flush_multibyte_target_full) {
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 12, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 12;
-        flush.blksize = 8;
-        flush.blkmax = 2;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        cxBufferPutString(&buf, "preparation");
-        size_t written = cxBufferWrite("teststring", 2, 5, &buf);
-        CX_TEST_ASSERT(written == 5);
-        CX_TEST_ASSERT(buf.pos == 11);
-        CX_TEST_ASSERT(buf.size == 11);
-        CX_TEST_ASSERT(target.pos == 10);
-        CX_TEST_ASSERT(target.size == 10);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "nteststring", 11));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "preparatio", 10));
-        // pop the misaligned byte from the buffer
-        cxBufferPop(&buf, 1, 1);
-        // write three more items, but only one fits into the target and one more into the buffer
-        written = cxBufferWrite("123456", 2, 3, &buf);
-        CX_TEST_ASSERT(written == 2);
-        CX_TEST_ASSERT(buf.pos == 12);
-        CX_TEST_ASSERT(buf.size == 12);
-        CX_TEST_ASSERT(target.pos == 12);
-        CX_TEST_ASSERT(target.size == 12);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "eststrin1234", 12));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "preparationt", 12));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_large_data_flush_target_full) {
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    // simulate that the target is full:
-    target.pos = target.size = 16;
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 0;
-        flush.blksize = 32;
-        flush.blkmax = 1;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        // write more bytes than the buffer can take, but the target is full
-        size_t written = cxBufferWrite("foobarfoobar", 1, 12, &buf);
-        CX_TEST_ASSERT(written == 0);
-        CX_TEST_ASSERT(buf.pos == 0);
-        CX_TEST_ASSERT(buf.size == 0);
-        CX_TEST_ASSERT(target.pos == 16);
-        CX_TEST_ASSERT(target.size == 16);
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
-CX_TEST(test_buffer_write_flush_at_threshold_target_full) {
-    CxBuffer buf, target;
-    // target does NOT auto-extend and can get completely full
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    // source may auto-extend but flushes at a certain threshold
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferPutString(&buf, "prep");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 16;
-        flush.blksize = 4;
-        flush.blkmax = 2;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        // step one - adding 6 bytes does not exceed the threshold
-        size_t written = cxBufferWrite("foobar", 1, 6, &buf);
-        CX_TEST_ASSERT(written == 6);
-        CX_TEST_ASSERT(buf.pos == 10);
-        CX_TEST_ASSERT(buf.size == 10);
-        CX_TEST_ASSERT(target.pos == 0);
-        CX_TEST_ASSERT(target.size == 0);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "prepfoobar", 10));
-        // step two - adding 8 bytes is two too many, two blocks are flushed
-        written = cxBufferWrite("12345678", 1, 8, &buf);
-        CX_TEST_ASSERT(written == 8);
-        CX_TEST_ASSERT(buf.pos == 10);
-        CX_TEST_ASSERT(buf.size == 10);
-        CX_TEST_ASSERT(target.pos == 8);
-        CX_TEST_ASSERT(target.size == 8);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "ar12345678", 10));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoob", 8));
-        // step three - cannot flush more, but can write 6 more bytes
-        written = cxBufferWrite("ABCDEFGH", 1, 8, &buf);
-        CX_TEST_ASSERT(written == 6);
-        CX_TEST_ASSERT(buf.pos == 16);
-        CX_TEST_ASSERT(buf.size == 16);
-        CX_TEST_ASSERT(target.pos == 8);
-        CX_TEST_ASSERT(target.size == 8);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "ar12345678ABCDEF", 16));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoob", 8));
-        // final test - cannot write anything more
-        written = cxBufferWrite("baz", 1, 3, &buf);
-        CX_TEST_ASSERT(written == 0);
-        CX_TEST_ASSERT(buf.pos == 16);
-        CX_TEST_ASSERT(buf.size == 16);
-        CX_TEST_ASSERT(target.pos == 8);
-        CX_TEST_ASSERT(target.size == 8);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "ar12345678ABCDEF", 16));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepfoob", 8));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
 CX_TEST(test_buffer_pop) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -1652,42 +1303,6 @@
     cxBufferDestroy(&buf);
 }
 
-CX_TEST(test_buffer_flush) {
-    CxBuffer buf, target;
-    cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    cxBufferInit(&buf, NULL, 8, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    cxBufferPutString(&buf, "prepare");
-    CX_TEST_DO {
-        CxBufferFlushConfig flush;
-        flush.threshold = 0;
-        flush.blksize = 2;
-        flush.blkmax = 2;
-        flush.target = &target;
-        flush.wfunc = cxBufferWriteFunc;
-        CX_TEST_ASSERT(0 == cxBufferEnableFlushing(&buf, flush));
-        CX_TEST_ASSERT(buf.size == 7);
-        buf.pos = 5;
-        size_t flushed = cxBufferFlush(&buf);
-        CX_TEST_ASSERT(flushed == flush.blkmax * flush.blksize);
-        CX_TEST_ASSERT(buf.pos == 1);
-        CX_TEST_ASSERT(buf.size == 3);
-        CX_TEST_ASSERT(target.pos == 4);
-        CX_TEST_ASSERT(target.size == 4);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "are", 3));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prep", 4));
-        flushed = cxBufferFlush(&buf);
-        CX_TEST_ASSERT(flushed == 1);
-        CX_TEST_ASSERT(buf.pos == 0);
-        CX_TEST_ASSERT(buf.size == 2);
-        CX_TEST_ASSERT(target.pos == 5);
-        CX_TEST_ASSERT(target.size == 5);
-        CX_TEST_ASSERT(0 == memcmp(buf.space, "re", 2));
-        CX_TEST_ASSERT(0 == memcmp(target.space, "prepa", 5));
-    }
-    cxBufferDestroy(&buf);
-    cxBufferDestroy(&target);
-}
-
 CX_TEST(test_buffer_get) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -1801,6 +1416,7 @@
     cx_test_register(suite, test_buffer_create_defaulted_allocator);
     cx_test_register(suite, test_buffer_minimum_capacity_sufficient);
     cx_test_register(suite, test_buffer_minimum_capacity_extend);
+    cx_test_register(suite, test_buffer_maximum_capacity);
     cx_test_register(suite, test_buffer_shrink);
     cx_test_register(suite, test_buffer_shrink_copy_on_write);
     cx_test_register(suite, test_buffer_shrink_copy_on_extend);
@@ -1845,7 +1461,6 @@
     cx_test_register(suite, test_buffer_write_multibyte_extend);
     cx_test_register(suite, test_buffer_write_copy_on_write);
     cx_test_register(suite, test_buffer_append);
-    cx_test_register(suite, test_buffer_append_flush);
     cx_test_register(suite, test_buffer_put_fit);
     cx_test_register(suite, test_buffer_put_discard);
     cx_test_register(suite, test_buffer_put_extend);
@@ -1858,18 +1473,9 @@
     cx_test_register(suite, test_buffer_terminate);
     cx_test_register(suite, test_buffer_write_size_overflow);
     cx_test_register(suite, test_buffer_write_capacity_overflow);
+    cx_test_register(suite, test_buffer_write_maximum_capacity_exceeded);
     cx_test_register(suite, test_buffer_write_only_overwrite);
-    cx_test_register(suite, test_buffer_write_flush_at_capacity);
-    cx_test_register(suite, test_buffer_write_flush_at_threshold);
-    cx_test_register(suite, test_buffer_write_flush_at_threshold_target_full);
-    cx_test_register(suite, test_buffer_write_flush_rate_limited_and_buffer_too_small);
-    cx_test_register(suite, test_buffer_write_flush_multibyte);
-    cx_test_register(suite, test_buffer_write_flush_misaligned);
-    cx_test_register(suite, test_buffer_write_flush_target_full);
-    cx_test_register(suite, test_buffer_write_flush_multibyte_target_full);
-    cx_test_register(suite, test_buffer_write_large_data_flush_target_full);
     cx_test_register(suite, test_buffer_pop);
-    cx_test_register(suite, test_buffer_flush);
     cx_test_register(suite, test_buffer_get);
     cx_test_register(suite, test_buffer_get_eof);
     cx_test_register(suite, test_buffer_read);

mercurial