diff --git a/include/rcutils/types/string_array.h b/include/rcutils/types/string_array.h index 743ea9e7..e627a4dd 100644 --- a/include/rcutils/types/string_array.h +++ b/include/rcutils/types/string_array.h @@ -133,6 +133,35 @@ rcutils_string_array_cmp( const rcutils_string_array_t * rhs, int * res); +/// Resize a string array, reclaiming removed resources. +/** + * This function changes the size of an existing string array. + * If the new size is larger, new entries are added to the end of the array and + * are zero- initialized. + * If the new size is smaller, entries are removed from the end of the array + * and their resources reclaimed. + * + * \par Note: + * Resizing to 0 is not a substitute for calling ::rcutils_string_array_fini. + * + * \par Note: + * If this function fails, \p string_array remains unchanged and should still + * be reclaimed with ::rcutils_string_array_fini. + * + * \param[inout] string_array object to be resized. + * \param[in] new_size the size the array should be changed to. + * \return `RCUTILS_RET_OK` if successful, or + * \return `RCUTILS_RET_INVALID_ARGUMENT` for invalid arguments, or + * \return `RCUTILS_RET_BAD_ALLOC` if memory allocation fails, or + * \return `RCUTILS_RET_ERROR` if an unknown error occurs. + */ +RCUTILS_PUBLIC +RCUTILS_WARN_UNUSED +rcutils_ret_t +rcutils_string_array_resize( + rcutils_string_array_t * string_array, + size_t new_size); + #ifdef __cplusplus } #endif diff --git a/src/string_array.c b/src/string_array.c index dffbb648..c91a1ebd 100644 --- a/src/string_array.c +++ b/src/string_array.c @@ -53,7 +53,7 @@ rcutils_string_array_init( string_array->size = size; string_array->data = allocator->zero_allocate(size, sizeof(char *), allocator->state); if (NULL == string_array->data && 0 != size) { - RCUTILS_SET_ERROR_MSG("failed to allocator string array"); + RCUTILS_SET_ERROR_MSG("failed to allocate string array"); return RCUTILS_RET_BAD_ALLOC; } string_array->allocator = *allocator; @@ -84,6 +84,7 @@ rcutils_string_array_fini(rcutils_string_array_t * string_array) } allocator->deallocate(string_array->data, allocator->state); string_array->data = NULL; + string_array->size = 0; return RCUTILS_RET_OK; } @@ -136,6 +137,62 @@ rcutils_string_array_cmp( return RCUTILS_RET_OK; } +rcutils_ret_t +rcutils_string_array_resize( + rcutils_string_array_t * string_array, + size_t new_size) +{ + RCUTILS_CHECK_FOR_NULL_WITH_MSG( + string_array, "string_array is null", return RCUTILS_RET_INVALID_ARGUMENT); + + if (string_array->size == new_size) { + return RCUTILS_RET_OK; + } + + rcutils_allocator_t * allocator = &string_array->allocator; + RCUTILS_CHECK_ALLOCATOR_WITH_MSG( + allocator, "allocator is invalid", return RCUTILS_RET_INVALID_ARGUMENT); + + // Stash entries being removed + rcutils_string_array_t to_reclaim = rcutils_get_zero_initialized_string_array(); + if (new_size < string_array->size) { + size_t num_removed = string_array->size - new_size; + rcutils_ret_t ret = rcutils_string_array_init(&to_reclaim, num_removed, allocator); + if (RCUTILS_RET_OK != ret) { + // rcutils_string_array_init should have already set an error message + return ret; + } + memcpy( + to_reclaim.data, &string_array->data[new_size], + to_reclaim.size * sizeof(char *)); + } + + char ** new_data = allocator->reallocate( + string_array->data, new_size * sizeof(char *), allocator->state); + if (NULL == new_data && 0 != new_size) { + RCUTILS_SET_ERROR_MSG("failed to allocate string array"); + for (size_t i = 0; i < to_reclaim.size; ++i) { + to_reclaim.data[i] = NULL; + } + rcutils_ret_t ret = rcutils_string_array_fini(&to_reclaim); + if (RCUTILS_RET_OK != ret) { + RCUTILS_SET_ERROR_MSG("memory was leaked during error handling"); + } + return RCUTILS_RET_BAD_ALLOC; + } + string_array->data = new_data; + + // Zero-initialize new entries + for (size_t i = string_array->size; i < new_size; ++i) { + string_array->data[i] = NULL; + } + + string_array->size = new_size; + + // Lastly, reclaim removed entries + return rcutils_string_array_fini(&to_reclaim); +} + #ifdef __cplusplus } #endif diff --git a/test/test_string_array.cpp b/test/test_string_array.cpp index 7bbdbcc6..0426b90d 100644 --- a/test/test_string_array.cpp +++ b/test/test_string_array.cpp @@ -15,6 +15,7 @@ #include "gtest/gtest.h" #include "./allocator_testing_utils.h" +#include "./time_bomb_allocator_testing_utils.h" #include "rcutils/error_handling.h" #include "rcutils/types/string_array.h" @@ -153,3 +154,105 @@ TEST(test_string_array, string_array_cmp) { ret = rcutils_string_array_fini(&incomplete_string_array); ASSERT_EQ(RCUTILS_RET_OK, ret); } + +TEST(test_string_array, string_array_resize) { + auto allocator = rcutils_get_default_allocator(); + auto failing_allocator = get_failing_allocator(); + auto invalid_allocator = rcutils_get_zero_initialized_allocator(); + auto time_bomb_allocator = get_time_bomb_allocator(); + rcutils_ret_t ret; + + ret = rcutils_string_array_resize(nullptr, 8); + ASSERT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + rcutils_reset_error(); + + // Start with 8 elements + rcutils_string_array_t sa0; + ret = rcutils_string_array_init(&sa0, 8, &allocator); + ASSERT_EQ(RCUTILS_RET_OK, ret); + + for (size_t i = 0; i < sa0.size; i++) { + const char val[] = {static_cast('a' + i), '\0'}; + sa0.data[i] = strdup(val); + } + + // Resize to same size (hot path) + ret = rcutils_string_array_resize(&sa0, sa0.size); + ASSERT_EQ(RCUTILS_RET_OK, ret); + + // Grow to 16 (with allocation failure) + sa0.allocator = failing_allocator; + ret = rcutils_string_array_resize(&sa0, 16); + EXPECT_EQ(RCUTILS_RET_BAD_ALLOC, ret); + EXPECT_EQ(8u, sa0.size); + rcutils_reset_error(); + + // Grow to 16 (with invalid allocator) + sa0.allocator = invalid_allocator; + ret = rcutils_string_array_resize(&sa0, 16); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + EXPECT_EQ(8u, sa0.size); + rcutils_reset_error(); + + // Grow to 16 + sa0.allocator = allocator; + ret = rcutils_string_array_resize(&sa0, 16); + ASSERT_EQ(RCUTILS_RET_OK, ret); + ASSERT_EQ(16u, sa0.size); + + // Check that existing data is intact + for (size_t i = 0; i < 8; i++) { + const char val[] = {static_cast('a' + i), '\0'}; + EXPECT_STREQ(val, sa0.data[i]); + } + + // Check that new elements are empty + for (size_t i = 8; i < sa0.size; i++) { + const char val[] = {static_cast('a' + i), '\0'}; + EXPECT_STREQ(nullptr, sa0.data[i]); + sa0.data[i] = strdup(val); + } + + // Shrink to 4 (with allocation failure) + sa0.allocator = failing_allocator; + ret = rcutils_string_array_resize(&sa0, 4); + EXPECT_EQ(RCUTILS_RET_BAD_ALLOC, ret); + EXPECT_EQ(16u, sa0.size); + rcutils_reset_error(); + + // Shrink to 4 (with delayed allocation failure) + set_time_bomb_allocator_realloc_count(time_bomb_allocator, 0); + sa0.allocator = time_bomb_allocator; + ret = rcutils_string_array_resize(&sa0, 4); + EXPECT_EQ(RCUTILS_RET_BAD_ALLOC, ret); + EXPECT_EQ(16u, sa0.size); + rcutils_reset_error(); + + // Shrink to 4 (with invalid allocator) + sa0.allocator = invalid_allocator; + ret = rcutils_string_array_resize(&sa0, 4); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, ret); + EXPECT_EQ(16u, sa0.size); + rcutils_reset_error(); + + // Shrink to 4 + sa0.allocator = allocator; + ret = rcutils_string_array_resize(&sa0, 4); + ASSERT_EQ(RCUTILS_RET_OK, ret); + ASSERT_EQ(4u, sa0.size); + + // Check that existing data is intact + for (size_t i = 0; i < sa0.size; i++) { + const char val[] = {static_cast('a' + i), '\0'}; + EXPECT_STREQ(val, sa0.data[i]); + } + + // Shrink to 0 + ret = rcutils_string_array_resize(&sa0, 0); + EXPECT_EQ(RCUTILS_RET_OK, ret); + EXPECT_EQ(0u, sa0.size); + + sa0.allocator = allocator; + ret = rcutils_string_array_fini(&sa0); + ASSERT_EQ(RCUTILS_RET_OK, ret); +}