improves cxBufferTerminate() default tip

Thu, 11 Dec 2025 23:47:46 +0100

author
Mike Becker <universe@uap-core.de>
date
Thu, 11 Dec 2025 23:47:46 +0100
changeset 1575
dde0c67a449b
parent 1574
cfbf4a3a9c11

improves cxBufferTerminate()

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	Thu Dec 11 23:07:24 2025 +0100
+++ b/CHANGELOG	Thu Dec 11 23:47:46 2025 +0100
@@ -8,6 +8,8 @@
  * adds CX_BUFFER_DO_NOT_FREE buffer flag
  * 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 cxBufferTerminate() to automatically shrink the buffer
+ * changes cxBufferTerminate() so that position and size are equal after successful operation
  * changes the members of CxJson and CxJsonValue
  * changes the return value of cxJsonObjIter() to CxMapIterator
  * changes CxTree structure so that it now inherits CX_COLLECTION_BASE
--- a/docs/Writerside/topics/about.md	Thu Dec 11 23:07:24 2025 +0100
+++ b/docs/Writerside/topics/about.md	Thu Dec 11 23:47:46 2025 +0100
@@ -35,6 +35,8 @@
 * adds CX_BUFFER_DO_NOT_FREE buffer flag
 * 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 cxBufferTerminate() to automatically shrink the buffer
+* changes cxBufferTerminate() so that position and size are equal after successful operation
 * changes the members of CxJson and CxJsonValue
 * changes the return value of cxJsonObjIter() to CxMapIterator
 * changes CxTree structure so that it now inherits CX_COLLECTION_BASE
--- a/docs/Writerside/topics/buffer.h.md	Thu Dec 11 23:07:24 2025 +0100
+++ b/docs/Writerside/topics/buffer.h.md	Thu Dec 11 23:47:46 2025 +0100
@@ -194,9 +194,12 @@
 
 All the above functions advance the buffer position by the number of bytes written 
 and cause the _size_ of the buffer to grow, if necessary, to contain all written bytes.
-On the other hand, `cxBufferTerminate()` writes a zero-byte at the current position,
+On the other hand, `cxBufferTerminate()` writes a zero-byte at the current position and shrinks the buffer,
 effectively creating a zero-terminated string whose size equals the buffer size.
 
+> If you use cxBufferTerminate() on a buffer with the `CX_BUFFER_COPY_ON_EXTEND` flag set, the shrink operation is skipped.
+> Using `cxBufferTerminate()` on a buffer with the `CX_BUFFER_COPY_ON_WRITE` flag set, will copy the entire memory just to add the zero-terminator.
+
 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.
 
--- a/src/buffer.c	Thu Dec 11 23:07:24 2025 +0100
+++ b/src/buffer.c	Thu Dec 11 23:47:46 2025 +0100
@@ -395,12 +395,27 @@
 }
 
 int cxBufferTerminate(CxBuffer *buffer) {
-    if (0 == cxBufferPut(buffer, 0)) {
-        buffer->size = buffer->pos - 1;
-        return 0;
+    // try to extend / shrink the buffer
+    if (buffer->pos >= buffer->capacity) {
+        if ((buffer->flags & CX_BUFFER_AUTO_EXTEND) == 0) {
+            return -1;
+        }
+        if (cxBufferReserve(buffer, buffer->pos + 1)) {
+            return -1; // LCOV_EXCL_LINE
+        }
     } else {
-        return -1;
+        buffer->size = buffer->pos;
+        cxBufferShrink(buffer, 1);
+        // set the capacity explicitly, in case shrink was skipped due to CoW
+        buffer->capacity = buffer->size + 1;
     }
+
+    // check if we are still on read-only memory
+    if (buffer_copy_on_write(buffer)) return -1;
+
+    // write the terminator and exit
+    buffer->space[buffer->pos] = '\0';
+    return 0;
 }
 
 size_t cxBufferPutString(
--- a/src/cx/buffer.h	Thu Dec 11 23:07:24 2025 +0100
+++ b/src/cx/buffer.h	Thu Dec 11 23:47:46 2025 +0100
@@ -545,14 +545,14 @@
 /**
  * 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, also sets the size to the current position and shrinks the buffer.
  *
  * 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.
  *
  * @param buffer the buffer to write to
  * @return zero, if the terminator could be written, non-zero otherwise
+ * @see cxBufferShrink()
  */
 cx_attr_nonnull
 CX_EXPORT int cxBufferTerminate(CxBuffer *buffer);
--- a/tests/test_buffer.c	Thu Dec 11 23:07:24 2025 +0100
+++ b/tests/test_buffer.c	Thu Dec 11 23:47:46 2025 +0100
@@ -260,13 +260,12 @@
         CxBuffer buf;
         cxBufferInit(&buf, NULL, 16, alloc, CX_BUFFER_FREE_CONTENTS);
         cxBufferPutString(&buf, "Testing");
-        cxBufferTerminate(&buf);
         CX_TEST_ASSERT(buf.capacity == 16);
         CX_TEST_ASSERT(buf.size == 7);
         cxBufferShrink(&buf, 4);
         CX_TEST_ASSERT(buf.capacity == 11);
         CX_TEST_ASSERT(buf.size == 7);
-        CX_TEST_ASSERT(memcmp(buf.space, "Testing", 8) == 0);
+        CX_TEST_ASSERT(memcmp(buf.space, "Testing", 7) == 0);
         cxBufferDestroy(&buf);
         CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
     }
@@ -1174,13 +1173,38 @@
         buf.flags |= CX_BUFFER_AUTO_EXTEND;
         CX_TEST_ASSERT(0 == cxBufferTerminate(&buf));
         CX_TEST_ASSERT(buf.size == 8);
-        CX_TEST_ASSERT(buf.pos == 9);
-        CX_TEST_ASSERT(buf.capacity > 8);
+        CX_TEST_ASSERT(buf.pos == 8);
+        CX_TEST_ASSERT(buf.capacity == 9);
         CX_TEST_ASSERT(0 == memcmp(buf.space, "preptest\0", 9));
     }
     cxBufferDestroy(&buf);
 }
 
+CX_TEST(test_buffer_terminate_copy_on_write) {
+    CxTestingAllocator talloc;
+    cx_testing_allocator_init(&talloc);
+    CxAllocator *alloc = &talloc.base;
+    CxBuffer buf;
+    cxBufferInit(&buf, "prepAAAAAA\0\0\0\0\0\0", 16, alloc,
+        CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_DO_NOT_FREE);
+    buf.capacity = 8;
+    buf.size = buf.pos = 4;
+    CX_TEST_DO {
+        CX_TEST_ASSERT(0 == cxBufferTerminate(&buf));
+        CX_TEST_ASSERT(buf.size == 4);
+        CX_TEST_ASSERT(buf.pos == 4);
+        CX_TEST_ASSERT(buf.capacity == 5);
+        CX_TEST_ASSERT(0 == memcmp(buf.space, "prep\0", 5));
+        // check if the memory was copied
+        CX_TEST_ASSERT(cx_testing_allocator_used(&talloc));
+        CX_TEST_ASSERT(!cx_testing_allocator_verify(&talloc));
+        cxFree(alloc, buf.space);
+        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
+    }
+    cxBufferDestroy(&buf);
+    cx_testing_allocator_destroy(&talloc);
+}
+
 CX_TEST(test_buffer_write_size_overflow) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -1471,6 +1495,7 @@
     cx_test_register(suite, test_buffer_put_string_copy_on_extend);
     cx_test_register(suite, test_buffer_put_string_copy_on_write);
     cx_test_register(suite, test_buffer_terminate);
+    cx_test_register(suite, test_buffer_terminate_copy_on_write);
     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);

mercurial