Tue, 16 Jan 2024 23:13:01 +0100
add cx_sprintf() variants - fixes #353
CHANGELOG | file | annotate | diff | comparison | revisions | |
src/cx/printf.h | file | annotate | diff | comparison | revisions | |
src/printf.c | file | annotate | diff | comparison | revisions | |
tests/test_printf.c | file | annotate | diff | comparison | revisions |
--- a/CHANGELOG Tue Jan 16 23:12:43 2024 +0100 +++ b/CHANGELOG Tue Jan 16 23:13:01 2024 +0100 @@ -4,6 +4,7 @@ * adds cxListFindRemove() * adds cxBufferReset() * adds cx_cmp_ptr() + * adds cx_sprintf() and several more variants * adds runtime constants to read out the actual SBO sizes * adds improved version of UCX 2 Test framework (now a self-contained header) * the cx_compare_func symbol is now also declared by compare.h
--- a/src/cx/printf.h Tue Jan 16 23:12:43 2024 +0100 +++ b/src/cx/printf.h Tue Jan 16 23:13:01 2024 +0100 @@ -164,6 +164,170 @@ #define cx_bprintf(buffer, fmt, ...) cx_fprintf((CxBuffer*)buffer, \ (cx_write_func) cxBufferWrite, fmt, __VA_ARGS__) + +/** + * An \c sprintf like function which reallocates the string when the buffer is not large enough. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * @param str a pointer to the string buffer + * @param len the current length of the buffer + * @param fmt the format string + * @param ... additional arguments + * @return the length of produced string + */ +#define cx_sprintf(str, len, fmt, ...) cx_sprintf_a(cxDefaultAllocator, str, len, fmt, __VA_ARGS__) + +/** + * An \c sprintf like function which reallocates the string when the buffer is not large enough. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * \attention The original buffer MUST have been allocated with the same allocator! + * + * @param alloc the allocator to use + * @param str a pointer to the string buffer + * @param len the current length of the buffer + * @param fmt the format string + * @param ... additional arguments + * @return the length of produced string + */ +__attribute__((__nonnull__(1, 2, 4), __format__(printf, 4, 5))) +int cx_sprintf_a(CxAllocator *alloc, char **str, size_t len, const char *fmt, ... ); + + +/** + * An \c sprintf like function which reallocates the string when the buffer is not large enough. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * @param str a pointer to the string buffer + * @param len the current length of the buffer + * @param fmt the format string + * @param ap argument list + * @return the length of produced string + */ +#define cx_vsprintf(str, len, fmt, ap) cx_vsprintf_a(cxDefaultAllocator, str, len, fmt, ap) + +/** + * An \c sprintf like function which reallocates the string when the buffer is not large enough. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * \attention The original buffer MUST have been allocated with the same allocator! + * + * @param alloc the allocator to use + * @param str a pointer to the string buffer + * @param len the current length of the buffer + * @param fmt the format string + * @param ap argument list + * @return the length of produced string + */ +__attribute__((__nonnull__)) +int cx_vsprintf_a(CxAllocator *alloc, char **str, size_t len, const char *fmt, va_list ap); + + +/** + * An \c sprintf like function which allocates a new string when the buffer is not large enough. + * + * The location of the resulting string will \em always be stored to \p str. When the buffer + * was sufficiently large, \p buf itself will be stored to the location of \p str. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * \remark When a new string needed to be allocated, the contents of \p buf will be + * poisoned after the call, because this function tries to produce the string in \p buf, first. + * + * @param buf a pointer to the buffer + * @param len the length of the buffer + * @param str a pointer to the location + * @param fmt the format string + * @param ... additional arguments + * @return the length of produced string + */ +#define cx_sprintf_s(buf, len, str, fmt, ...) cx_sprintf_sa(cxDefaultAllocator, buf, len, str, fmt, __VA_ARGS__) + +/** + * An \c sprintf like function which allocates a new string when the buffer is not large enough. + * + * The location of the resulting string will \em always be stored to \p str. When the buffer + * was sufficiently large, \p buf itself will be stored to the location of \p str. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * \remark When a new string needed to be allocated, the contents of \p buf will be + * poisoned after the call, because this function tries to produce the string in \p buf, first. + * + * @param alloc the allocator to use + * @param buf a pointer to the buffer + * @param len the length of the buffer + * @param str a pointer to the location + * @param fmt the format string + * @param ... additional arguments + * @return the length of produced string + */ +__attribute__((__nonnull__(1, 2, 4, 5), __format__(printf, 5, 6))) +int cx_sprintf_sa(CxAllocator *alloc, char *buf, size_t len, char **str, const char *fmt, ... ); + +/** + * An \c sprintf like function which allocates a new string when the buffer is not large enough. + * + * The location of the resulting string will \em always be stored to \p str. When the buffer + * was sufficiently large, \p buf itself will be stored to the location of \p str. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * \remark When a new string needed to be allocated, the contents of \p buf will be + * poisoned after the call, because this function tries to produce the string in \p buf, first. + * + * @param buf a pointer to the buffer + * @param len the length of the buffer + * @param str a pointer to the location + * @param fmt the format string + * @param ap argument list + * @return the length of produced string + */ +#define cx_vsprintf_s(buf, len, str, fmt, ap) cx_vsprintf_sa(cxDefaultAllocator, buf, len, str, fmt, ap) + +/** + * An \c sprintf like function which allocates a new string when the buffer is not large enough. + * + * The location of the resulting string will \em always be stored to \p str. When the buffer + * was sufficiently large, \p buf itself will be stored to the location of \p str. + * + * \note The resulting string is guaranteed to be zero-terminated. + * That means, when the buffer needed to be reallocated, the new size of the buffer will be + * the length returned by this function plus one. + * + * \remark When a new string needed to be allocated, the contents of \p buf will be + * poisoned after the call, because this function tries to produce the string in \p buf, first. + * + * @param alloc the allocator to use + * @param buf a pointer to the buffer + * @param len the length of the buffer + * @param str a pointer to the location + * @param fmt the format string + * @param ap argument list + * @return the length of produced string + */ +__attribute__((__nonnull__)) +int cx_vsprintf_sa(CxAllocator *alloc, char *buf, size_t len, char **str, const char *fmt, va_list ap); + + #ifdef __cplusplus } // extern "C" #endif
--- a/src/printf.c Tue Jan 16 23:12:43 2024 +0100 +++ b/src/printf.c Tue Jan 16 23:13:01 2024 +0100 @@ -127,3 +127,73 @@ return s; } +int cx_sprintf_a(CxAllocator *alloc, char **str, size_t len, const char *fmt, ... ) { + va_list ap; + va_start(ap, fmt); + int ret = cx_vsprintf_a(alloc, str, len, fmt, ap); + va_end(ap); + return ret; +} + +int cx_vsprintf_a(CxAllocator *alloc, char **str, size_t len, const char *fmt, va_list ap) { + va_list ap2; + va_copy(ap2, ap); + int ret = vsnprintf(*str, len, fmt, ap); + if (ret < 0 || ((unsigned)ret) < len) { + va_end(ap2); + return ret; + } else { + unsigned newlen = ret + 1; + char *ptr = cxRealloc(alloc, *str, newlen); + if (ptr) { + int newret = vsnprintf(ptr, newlen, fmt, ap2); + va_end(ap2); + if (newret < 0) { + cxFree(alloc, ptr); + return ret; + } else { + *str = ptr; + return newret; + } + } else { + va_end(ap2); + return ret; + } + } +} + +int cx_sprintf_sa(CxAllocator *alloc, char *buf, size_t len, char **str, const char *fmt, ... ) { + va_list ap; + va_start(ap, fmt); + int ret = cx_vsprintf_sa(alloc, buf, len, str, fmt, ap); + va_end(ap); + return ret; +} + +int cx_vsprintf_sa(CxAllocator *alloc, char *buf, size_t len, char **str, const char *fmt, va_list ap) { + va_list ap2; + va_copy(ap2, ap); + int ret = vsnprintf(buf, len, fmt, ap); + *str = buf; + if (ret < 0 || ((unsigned)ret) < len) { + va_end(ap2); + return ret; + } else { + unsigned newlen = ret + 1; + char *ptr = cxMalloc(alloc, newlen); + if (ptr) { + int newret = vsnprintf(ptr, newlen, fmt, ap2); + va_end(ap2); + if (newret < 0) { + cxFree(alloc, ptr); + return ret; + } else { + *str = ptr; + return newret; + } + } else { + va_end(ap2); + return ret; + } + } +}
--- a/tests/test_printf.c Tue Jan 16 23:12:43 2024 +0100 +++ b/tests/test_printf.c Tue Jan 16 23:13:01 2024 +0100 @@ -294,6 +294,107 @@ free(expected); } +CX_TEST(test_sprintf_no_realloc) { + char *buf = malloc(16); + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + CX_TEST_DO { + char *oldbuf = buf; + size_t len = cx_sprintf_a(alloc, &buf, 16, "Test %d %s", 47, "string"); + CX_TEST_ASSERT(oldbuf == buf); + CX_TEST_ASSERT(len == 14); + CX_TEST_ASSERT(0 == memcmp(buf, "Test 47 string", 15)); + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); + free(buf); +} + +CX_TEST(test_sprintf_realloc) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + char *buf = cxMalloc(alloc, 8); + CX_TEST_DO { + size_t len = cx_sprintf_a(alloc, &buf, 8, "Test %d %s", 47, "foobar"); + CX_TEST_ASSERT(len == 14); + CX_TEST_ASSERT(0 == memcmp(buf, "Test 47 foobar", 15)); + cxFree(alloc, buf); + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + +CX_TEST(test_sprintf_realloc_to_fit_terminator) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + // make it so that only the zero-terminator does not fit + char *buf = cxMalloc(alloc, 14); + CX_TEST_DO { + size_t len = cx_sprintf_a(alloc, &buf, 14, "Test %d %s", 13, "string"); + CX_TEST_ASSERT(len == 14); + CX_TEST_ASSERT(0 == memcmp(buf, "Test 13 string", 15)); + cxFree(alloc, buf); + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + +CX_TEST(test_sprintf_s_no_alloc) { + char buf[16]; + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + CX_TEST_DO { + char *str; + size_t len = cx_sprintf_sa(alloc, buf, 16, &str, "Test %d %s", 47, "string"); + CX_TEST_ASSERT(str == buf); + CX_TEST_ASSERT(len == 14); + CX_TEST_ASSERT(0 == memcmp(buf, "Test 47 string", 15)); + CX_TEST_ASSERT(0 == memcmp(str, "Test 47 string", 15)); + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + +CX_TEST(test_sprintf_s_alloc) { + char buf[16]; + memcpy(buf, "0123456789abcdef", 16); + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + CX_TEST_DO { + char *str; + size_t len = cx_sprintf_sa(alloc, buf, 16, &str, "Hello %d %s", 4711, "larger string"); + CX_TEST_ASSERT(str != buf); + CX_TEST_ASSERT(len == 24); + CX_TEST_ASSERT(0 == memcmp(str, "Hello 4711 larger string", 25)); + cxFree(alloc, str); + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + +CX_TEST(test_sprintf_s_alloc_to_fit_terminator) { + char buf[16]; + memcpy(buf, "0123456789abcdef", 16); + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + CX_TEST_DO { + char *str; + size_t len = cx_sprintf_sa(alloc, buf, 16, &str, "Hello %d %s", 112, "string"); + CX_TEST_ASSERT(str != buf); + CX_TEST_ASSERT(len == 16); + CX_TEST_ASSERT(0 == memcmp(str, "Hello 112 string", 17)); // include terminator + cxFree(alloc, str); + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + CxTestSuite *cx_test_suite_printf(void) { CxTestSuite *suite = cx_test_suite_new("printf"); @@ -303,6 +404,12 @@ cx_test_register(suite, test_fprintf); cx_test_register(suite, test_asprintf); cx_test_register(suite, test_asprintf_large_string); + cx_test_register(suite, test_sprintf_no_realloc); + cx_test_register(suite, test_sprintf_realloc); + cx_test_register(suite, test_sprintf_realloc_to_fit_terminator); + cx_test_register(suite, test_sprintf_s_no_alloc); + cx_test_register(suite, test_sprintf_s_alloc); + cx_test_register(suite, test_sprintf_s_alloc_to_fit_terminator); return suite; }