# HG changeset patch # User Mike Becker # Date 1765493266 -3600 # Node ID dde0c67a449bd5017a7d974a465ec2696fe3d247 # Parent cfbf4a3a9c11f9e65e5f0395ed3ccda6b4bf1c3b improves cxBufferTerminate() diff -r cfbf4a3a9c11 -r dde0c67a449b CHANGELOG --- 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 diff -r cfbf4a3a9c11 -r dde0c67a449b docs/Writerside/topics/about.md --- 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 diff -r cfbf4a3a9c11 -r dde0c67a449b docs/Writerside/topics/buffer.h.md --- 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. diff -r cfbf4a3a9c11 -r dde0c67a449b src/buffer.c --- 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( diff -r cfbf4a3a9c11 -r dde0c67a449b src/cx/buffer.h --- 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); diff -r cfbf4a3a9c11 -r dde0c67a449b tests/test_buffer.c --- 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);