Wed, 26 Nov 2025 23:35:25 +0100
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 = ⌖ + 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 = ⌖ + 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);