fixes that cxBufferWrite() could auto-extend the buffer beyond the configured threshold

Wed, 26 Nov 2025 23:35:25 +0100

author
Mike Becker <universe@uap-core.de>
date
Wed, 26 Nov 2025 23:35:25 +0100
changeset 1516
95b094472e9a
parent 1515
f024313c08f1
child 1517
1fe3df1c7c67

fixes that cxBufferWrite() could auto-extend the buffer beyond the configured threshold

CHANGELOG file | annotate | diff | comparison | revisions
docs/Writerside/topics/about.md file | annotate | diff | comparison | revisions
src/buffer.c file | annotate | diff | comparison | revisions
tests/test_buffer.c file | annotate | diff | comparison | revisions
--- a/CHANGELOG	Wed Nov 26 23:22:03 2025 +0100
+++ b/CHANGELOG	Wed Nov 26 23:35:25 2025 +0100
@@ -51,6 +51,7 @@
  * fixes ineffective overflow check in cx_strcat() family of functions
  * fixes errno value after failing cxBufferSeek() to be consistently EINVAL
  * fixes implementation of cxBufferTerminate()
+ * fixes that cxBufferWrite() could auto-extend the buffer beyond the configured threshold
  * fixes allocator arguments for some printf.h functions not being const
  * fixes that cx_tree_search() did not investigate subtrees with equally good distance
  * fixes that memory was freed by the wrong allocator in cx_vasprintf_a() when the underlying vsnprintf() failed
--- a/docs/Writerside/topics/about.md	Wed Nov 26 23:22:03 2025 +0100
+++ b/docs/Writerside/topics/about.md	Wed Nov 26 23:35:25 2025 +0100
@@ -78,6 +78,7 @@
 * fixes ineffective overflow check in cx_strcat() family of functions
 * fixes errno value after failing cxBufferSeek() to be consistently EINVAL
 * fixes implementation of cxBufferTerminate()
+* fixes that cxBufferWrite() could auto-extend the buffer beyond the configured threshold
 * fixes allocator arguments for some printf.h functions not being const
 * fixes that cx_tree_search() did not investigate subtrees with equally good distance
 * fixes that memory was freed by the wrong allocator in cx_vasprintf_a() when the underlying vsnprintf() failed
--- a/src/buffer.c	Wed Nov 26 23:22:03 2025 +0100
+++ b/src/buffer.c	Wed Nov 26 23:35:25 2025 +0100
@@ -261,32 +261,35 @@
     }
 }
 
+static size_t cx_buffer_calculate_minimum_capacity(size_t mincap) {
+    unsigned long pagesize = 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)
+    }
+    return mincap;
+}
+
 int cxBufferMinimumCapacity(CxBuffer *buffer, size_t newcap) {
     if (newcap <= buffer->capacity) {
         return 0;
     }
-
-    unsigned long pagesize = 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)
-    }
-
+    newcap = cx_buffer_calculate_minimum_capacity(newcap);
     return cxBufferReserve(buffer, newcap);
 }
 
@@ -392,8 +395,17 @@
     bool perform_flush = false;
     if (required > buffer->capacity) {
         if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-            if (buffer->flush != NULL && required > buffer->flush->threshold) {
-                perform_flush = true;
+            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
--- a/tests/test_buffer.c	Wed Nov 26 23:22:03 2025 +0100
+++ b/tests/test_buffer.c	Wed Nov 26 23:35:25 2025 +0100
@@ -1478,7 +1478,7 @@
         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
+        // final test - cannot write anything more
         written = cxBufferWrite("baz", 1, 3, &buf);
         CX_TEST_ASSERT(written == 0);
         CX_TEST_ASSERT(buf.pos == 8);
@@ -1492,6 +1492,98 @@
     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_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_flush) {
     CxBuffer buf, target;
     cxBufferInit(&target, NULL, 8, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
@@ -1700,10 +1792,12 @@
     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_flush);
     cx_test_register(suite, test_buffer_get);
     cx_test_register(suite, test_buffer_get_eof);

mercurial