diff --git a/0xtest/unit/test_str_builder.c b/0xtest/unit/test_str_builder.c new file mode 100644 index 0000000..c35aebb --- /dev/null +++ b/0xtest/unit/test_str_builder.c @@ -0,0 +1,85 @@ +/*! + * @copyright + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +#include "0xc/std/str_builder.h" +#include "0xc/sys/check.h" +#include "0xc/sys/unit.h" + +#include + +static void +UNIT_TEST_FUNC(str_builder_init)( + __unused const unit_t *un) +{ + char buff[10] = ""; + xc_str_builder_t sb; + bool result = false; + + result = xc_str_builder_init(&sb, buff, sizeof(buff)); + checkint("valid initialization succeeds", result, true); + checkint("length is initialized to 0", sb.length, 0); + checkint("capacity is correctly set", sb.capacity, 10); + + result = xc_str_builder_init(NULL, buff, 10); + checkint("null builder fails safely", result, false); + + result = xc_str_builder_init(&sb, NULL, 10); + checkint("null buffer fails safely", result, false); + + result = xc_str_builder_init(&sb, buff, 0); + checkint("zero capacity fails safely", result, false); +} + +static void +UNIT_TEST_FUNC(str_builder_append_overflow)( + __unused const unit_t *un) +{ + char buff[12] = ""; + xc_str_builder_t sb; + bool result = false; + + xc_str_builder_init(&sb, buff, sizeof(buff)); + + result = xc_str_builder_append(&sb, "Hello"); + checkint("normal append succeeds", result, true); + checkint("length updated correctly", sb.length, 5); + checkcstr("buffer contains Hello", sb.buffer, "Hello"); + + result = xc_str_builder_append(&sb, " World"); + checkint("second append succeeds", result, true); + checkint("length updated to 11", sb.length, 11); + checkcstr("buffer contains Hello World", sb.buffer, "Hello World"); + + /* OVERFLOW TEST: Capacity is 12. Length is 11. */ + result = xc_str_builder_append(&sb, "!"); + checkint("overflow append fails safely", result, false); + checkint("length remains 11", sb.length, 11); + checkcstr("buffer untouched by overflow", sb.buffer, "Hello World"); +} + +static void +UNIT_TEST_FUNC(str_builder_format_truncation)( + __unused const unit_t *un) +{ + char buff[16] = ""; + xc_str_builder_t sb; + bool result = false; + + xc_str_builder_init(&sb, buff, sizeof(buff)); + + result = xc_str_builder_appendf(&sb, "Age: %d", 25); + checkint("formatted append succeeds", result, true); + checkcstr("buffer formatted correctly", sb.buffer, "Age: 25"); + + /* TRUNCATION TEST: Try to format a string that exceeds remaining space */ + result = xc_str_builder_appendf(&sb, " is a very long string"); + checkint("truncating format fails safely", result, false); + checkcstr("buffer untouched by truncation", sb.buffer, "Age: 25"); +} + +UNIT_TEST(str_builder_init); +UNIT_TEST(str_builder_append_overflow); +UNIT_TEST(str_builder_format_truncation); \ No newline at end of file diff --git a/src/0xc/std/str_builder.c b/src/0xc/std/str_builder.c new file mode 100644 index 0000000..800bb91 --- /dev/null +++ b/src/0xc/std/str_builder.c @@ -0,0 +1,130 @@ +/*! + * @copyright + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +#include "str_builder.h" +#include +#include + +bool xc_str_builder_init( + xc_str_builder_t *sb, + char *buffer, + size_t capacity ) +{ + if (!sb || !buffer || !capacity) { + return false; + } + + sb->buffer = buffer; + sb->capacity = capacity; + sb->length = 0; + sb->buffer[0] = '\0'; + + return true; +} + +bool xc_str_builder_append( + xc_str_builder_t *sb, + const char *suffix ) +{ + if (!sb || !suffix) { + return false; + } + + size_t suffix_len = strlen(suffix); + if (sb->length + suffix_len >= sb->capacity) { + return false; + } + + memcpy(sb->buffer + sb->length, suffix, suffix_len); + sb->length += suffix_len; + sb->buffer[sb->length] = '\0'; + + return true; +} + +bool xc_str_builder_appendf( + xc_str_builder_t *sb, + const char *format, + ... ) +{ + if (!sb || !format) { + return false; + } + + size_t remaining = sb->capacity - sb->length; + if (!remaining) { + return false; + } + + va_list args; + va_start(args, format); + + int written = vsnprintf(sb->buffer + sb->length, remaining, format, args); + + va_end(args); + + if (written < 0) { + return false; + } + + // If it truncated, UNDO the partial write! + if ((size_t)written >= remaining) { + sb->buffer[sb->length] = '\0'; + return false; + } + + sb->length += (size_t)written; + + return true; +} + +bool xc_str_builder_append_char( + xc_str_builder_t *sb, + char c ) +{ + if (!sb) { + return false; + } + + if (sb->length + 1 >= sb->capacity) { + return false; + } + + sb->buffer[sb->length] = c; + sb->length++; + sb->buffer[sb->length] = '\0'; + + return true; +} + +void xc_str_builder_clear( + xc_str_builder_t *sb ) +{ + if (!sb) { + return; + } + + sb->length = 0; + sb->buffer[0] = '\0'; +} + +bool xc_str_builder_truncate( + xc_str_builder_t *sb, + size_t new_length ) +{ + if (!sb) { + return false; + } + + if (new_length > sb->length) { + return false; + } + + sb->length = new_length; + sb->buffer[sb->length] = '\0'; + + return true; +} \ No newline at end of file diff --git a/src/0xc/std/str_builder.h b/src/0xc/std/str_builder.h new file mode 100644 index 0000000..3e09e37 --- /dev/null +++ b/src/0xc/std/str_builder.h @@ -0,0 +1,145 @@ +/*! + * @copyright + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * @header + * Bounds-tracked string builder functions. + */ +#ifndef __ZX_STD_STR_BUILDER_H +#define __ZX_STD_STR_BUILDER_H + +#include <0xc/std/api.h> + +// IWYU pragma: begin_exports +#include +#include +#include +// IWYU pragma: end_exports + +__API_HEADER_BEGIN(c, nonnull, single) + +// MARK: Public + +/*! + * @typedef xc_str_builder_t + * Wraps a statically-allocated character array to provide bounds-tracked + * string building and concatenation. + */ +typedef struct { + char *buffer; + size_t capacity; + size_t length; +} xc_str_builder_t; + +/*! + * @function xc_str_builder_init + * Initializes the builder, wrapping an existing buffer safely. + * + * @param SB + * The string builder instance to initialize. + * + * @param BUFFER + * The statically-allocated destination buffer. + * + * @param CAPACITY + * The total capacity of the buffer (including the null terminator). + * + * @result + * Returns true if successfully initialized, false if arguments are invalid. + */ +bool xc_str_builder_init( + xc_str_builder_t *sb, + char *buffer, + size_t capacity +); + +/*! + * @function xc_str_builder_append + * Appends a null-terminated string to the builder. + * + * @param SB + * The string builder instance. + * + * @param SUFFIX + * The null-terminated string to append. + * + * @result + * Returns true if the string was successfully appended. Returns false if the + * operation would exceed the buffer's capacity. + */ +bool xc_str_builder_append( + xc_str_builder_t *sb, + const char *suffix +); + +/*! + * @function xc_str_builder_appendf + * Safely formats and appends data without silent truncation. + * + * @param SB + * The string builder instance. + * + * @param FORMAT + * The format string, followed by format arguments. + * + * @result + * Returns true if the formatted string was completely appended. Returns false + * if truncation occurred or arguments were invalid, leaving the buffer intact. + */ +bool xc_str_builder_appendf( + xc_str_builder_t *sb, + const char *format, + ... +); + +/*! + * @function xc_str_builder_append_char + * Appends a single character safely using O(1) performance. + * + * @param SB + * The string builder instance. + * + * @param C + * The character to append. + * + * @result + * Returns true if the character was appended, false if capacity was exceeded. + */ +bool xc_str_builder_append_char( + xc_str_builder_t *sb, + char c +); + +/*! + * @function xc_str_builder_clear + * Resets the string builder length to 0, allowing reuse of the buffer. + * + * @param SB + * The string builder instance. + */ +void xc_str_builder_clear( + xc_str_builder_t *sb +); + +/*! + * @function xc_str_builder_truncate + * Safely shortens the string to the specified length. + * + * @param SB + * The string builder instance. + * + * @param NEW_LENGTH + * The length to shorten the string to. + * + * @result + * Returns true on success, false if the new length exceeds current length. + */ +bool xc_str_builder_truncate( + xc_str_builder_t *sb, + size_t new_length +); + +__API_HEADER_END(c, nonnull, single) + +#endif // __ZX_STD_STR_BUILDER_H \ No newline at end of file