diff --git a/libs/utils/gtest/src/ConvertUtilsErrorInjectionTestSuite.cc b/libs/utils/gtest/src/ConvertUtilsErrorInjectionTestSuite.cc index ecd160f5d..958a0b404 100644 --- a/libs/utils/gtest/src/ConvertUtilsErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/ConvertUtilsErrorInjectionTestSuite.cc @@ -34,6 +34,7 @@ class ConvertUtilsWithErrorInjectionTestSuite : public ::testing::Test { celix_ei_expect_celix_arrayList_addLong(nullptr, 0, CELIX_SUCCESS); celix_ei_expect_open_memstream(nullptr, 0, nullptr); celix_ei_expect_fputs(nullptr, 0, 0); + celix_ei_expect_fputc(nullptr, 0, 0); celix_err_printErrors(stderr, nullptr, nullptr); } @@ -116,4 +117,29 @@ TEST_F(ConvertUtilsWithErrorInjectionTestSuite, LongArrayToStringTest) { result = celix_utils_longArrayListToString(list); //Then the result is null EXPECT_EQ(nullptr, result); -} \ No newline at end of file +} + +TEST_F(ConvertUtilsWithErrorInjectionTestSuite, StringToStringArrayTest) { + // Given an error injection for open_memstream (on the second call) + celix_ei_expect_open_memstream((void*)celix_utils_convertStringToStringArrayList, 1, nullptr, 2); + // When calling celix_utils_convertStringToStringArrayList + celix_array_list_t* result; + celix_status_t status = celix_utils_convertStringToStringArrayList("a,b,c", nullptr, &result); + // Then the result is null and the status is ENOMEM + EXPECT_EQ(status, CELIX_ENOMEM); + EXPECT_EQ(nullptr, result); + + // Given an error injection for fputc + celix_ei_expect_fputc((void*)celix_utils_convertStringToStringArrayList, 1, EOF); + // When calling celix_utils_convertStringToStringArrayList + status = celix_utils_convertStringToStringArrayList("a,b,c", nullptr, &result); + // Then the result is null and the status is ENOMEM + EXPECT_EQ(status, CELIX_ENOMEM); + + // Given an error injection for fputc (on writing an escaped char) + celix_ei_expect_fputc((void*)celix_utils_convertStringToStringArrayList, 1, EOF); + // When calling celix_utils_convertStringToStringArrayList + status = celix_utils_convertStringToStringArrayList(R"(\\)", nullptr, &result); + // Then the result is null and the status is ENOMEM + EXPECT_EQ(status, CELIX_ENOMEM); +} diff --git a/libs/utils/gtest/src/ConvertUtilsTestSuite.cc b/libs/utils/gtest/src/ConvertUtilsTestSuite.cc index 165519267..d0aa8d270 100644 --- a/libs/utils/gtest/src/ConvertUtilsTestSuite.cc +++ b/libs/utils/gtest/src/ConvertUtilsTestSuite.cc @@ -30,7 +30,7 @@ class ConvertUtilsTestSuite : public ::testing::Test { public: ~ConvertUtilsTestSuite() noexcept override { celix_err_printErrors(stderr, nullptr, nullptr); } - void checkVersion(const celix_version_t* version, int major, int minor, int micro, const char* qualifier) { + static void checkVersion(const celix_version_t* version, int major, int minor, int micro, const char* qualifier) { EXPECT_TRUE(version != nullptr); if (version) { EXPECT_EQ(major, celix_version_getMajor(version)); @@ -114,8 +114,9 @@ TEST_F(ConvertUtilsTestSuite, ConvertToDoubleTest) { EXPECT_EQ(1.0, result); EXPECT_FALSE(converted); - //test for a string with a invalid number + //test for a string with an invalid number result = celix_utils_convertStringToDouble("10.5A", 0, &converted); + EXPECT_EQ(0, result); EXPECT_FALSE(converted); //test for a string with a number and a negative sign @@ -139,6 +140,7 @@ TEST_F(ConvertUtilsTestSuite, ConvertToDoubleTest) { //test for a convert with an invalid double value with trailing info result = celix_utils_convertStringToDouble("11.1.2", 0, &converted); + EXPECT_EQ(0, result); EXPECT_FALSE(converted); //test for a convert with a double value with trailing whitespaces @@ -194,16 +196,17 @@ TEST_F(ConvertUtilsTestSuite, ConvertToBoolTest) { EXPECT_EQ(true, result); //test for a convert with a bool value with trailing chars - result = celix_utils_convertStringToBool("true and ok", 0, &converted); + result = celix_utils_convertStringToBool("true and ok", false, &converted); + EXPECT_FALSE(result); EXPECT_FALSE(converted); //test for a convert with a bool value with trailing whitespaces - result = celix_utils_convertStringToBool("true \t\n", 0, &converted); + result = celix_utils_convertStringToBool("true \t\n", false, &converted); EXPECT_TRUE(result); EXPECT_TRUE(converted); //test for a convert with a bool value with starting and trailing whitespaces - result = celix_utils_convertStringToBool("\t false \t\n", 0, &converted); + result = celix_utils_convertStringToBool("\t false \t\n", false, &converted); EXPECT_FALSE(result); EXPECT_TRUE(converted); @@ -345,7 +348,7 @@ TEST_F(ConvertUtilsTestSuite, LongArrayToStringTest) { celix_arrayList_addLong(list, 3L); char* result = celix_utils_longArrayListToString(list); - EXPECT_STREQ("1, 2, 3", result); + EXPECT_STREQ("1,2,3", result); free(result); celix_autoptr(celix_array_list_t) emptyList = celix_arrayList_create(); @@ -415,7 +418,7 @@ TEST_F(ConvertUtilsTestSuite, BoolArrayToStringTest) { celix_arrayList_addBool(list, true); char* result = celix_utils_boolArrayListToString(list); - EXPECT_STREQ("true, false, true", result); + EXPECT_STREQ("true,false,true", result); free(result); // NOTE celix_utils_boolArrayListToString uses the same generic function as is used in @@ -434,6 +437,33 @@ TEST_F(ConvertUtilsTestSuite, ConvertToStringArrayList) { EXPECT_STREQ("c", (char*)celix_arrayList_get(result, 2)); celix_arrayList_destroy(result); + convertState = celix_utils_convertStringToStringArrayList(R"(a,b\\\,,c)", nullptr, &result); + EXPECT_EQ(CELIX_SUCCESS, convertState); + EXPECT_TRUE(result != nullptr); + EXPECT_EQ(3, celix_arrayList_size(result)); + EXPECT_STREQ("a", celix_arrayList_getString(result, 0)); + EXPECT_STREQ("b\\,", celix_arrayList_getString(result, 1)); + EXPECT_STREQ("c", celix_arrayList_getString(result, 2)); + celix_arrayList_destroy(result); + + convertState = celix_utils_convertStringToStringArrayList("a,,b,", nullptr, &result); //4 entries, second and last are empty strings + EXPECT_EQ(CELIX_SUCCESS, convertState); + EXPECT_TRUE(result != nullptr); + EXPECT_EQ(4, celix_arrayList_size(result)); + EXPECT_STREQ("a", celix_arrayList_getString(result, 0)); + EXPECT_STREQ("", celix_arrayList_getString(result, 1)); + EXPECT_STREQ("b", celix_arrayList_getString(result, 2)); + EXPECT_STREQ("", celix_arrayList_getString(result, 3)); + celix_arrayList_destroy(result); + + //invalid escape sequence + convertState = celix_utils_convertStringToStringArrayList(R"(a,b\c,d)", nullptr, &result); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, convertState); + EXPECT_TRUE(result == nullptr); + convertState = celix_utils_convertStringToStringArrayList(R"(a,b,c\)", nullptr, &result); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, convertState); + EXPECT_TRUE(result == nullptr); + // NOTE celix_utils_convertStringToStringArrayList uses the same generic function as is used in // celix_utils_convertStringToLongArrayList and because celix_utils_convertStringToLongArrayList is already // tested, we only test a few cases here. @@ -441,17 +471,29 @@ TEST_F(ConvertUtilsTestSuite, ConvertToStringArrayList) { TEST_F(ConvertUtilsTestSuite, StringArrayToStringTest) { celix_autoptr(celix_array_list_t) list = celix_arrayList_create(); - celix_arrayList_add(list, (void*)"a"); - celix_arrayList_add(list, (void*)"b"); - celix_arrayList_add(list, (void*)"c"); + celix_arrayList_addString(list, "a"); + celix_arrayList_addString(list, "b"); + celix_arrayList_addString(list, "c"); char* result = celix_utils_stringArrayListToString(list); - EXPECT_STREQ("a, b, c", result); + EXPECT_STREQ("a,b,c", result); free(result); - // NOTE celix_utils_stringArrayListToString uses the same generic function as is used in - // celix_utils_longArrayListToString and because celix_utils_longArrayListToString is already - // tested, we only test a few cases here. + celix_arrayList_addString(list, "d\\,"); + celix_arrayList_addString(list, "e"); + result = celix_utils_stringArrayListToString(list); + EXPECT_STREQ(R"(a,b,c,d\\\,,e)", result); + + //Check if the result can be converted back to an equal list + celix_array_list_t* listResult; + celix_status_t convertState = celix_utils_convertStringToStringArrayList(result, nullptr, &listResult); + EXPECT_EQ(CELIX_SUCCESS, convertState); + EXPECT_TRUE(listResult != nullptr); + + EXPECT_EQ(celix_arrayList_size(list), celix_arrayList_size(listResult)); + for (int i = 0; i < celix_arrayList_size(list); ++i) { + EXPECT_STREQ((char*)celix_arrayList_get(list, i), (char*)celix_arrayList_get(listResult, i)); + } } TEST_F(ConvertUtilsTestSuite, ConvertToVersionArrayList) { @@ -480,7 +522,7 @@ TEST_F(ConvertUtilsTestSuite, VersionArrayToStringTest) { celix_arrayList_add(list, v3); char* result = celix_utils_versionArrayListToString(list); - EXPECT_STREQ("1.2.3, 2.3.4, 3.4.5.qualifier", result); + EXPECT_STREQ("1.2.3,2.3.4,3.4.5.qualifier", result); free(result); // NOTE celix_utils_versionArrayListToString uses the same generic function as is used in diff --git a/libs/utils/include/celix_array_list.h b/libs/utils/include/celix_array_list.h index 77ae1a1e8..991a8b2fd 100644 --- a/libs/utils/include/celix_array_list.h +++ b/libs/utils/include/celix_array_list.h @@ -41,7 +41,7 @@ extern "C" { #endif typedef union celix_array_list_entry { - void *voidPtrVal; + void* voidPtrVal; const char* strVal; int intVal; long int longVal; diff --git a/libs/utils/include/celix_convert_utils.h b/libs/utils/include/celix_convert_utils.h index d5fa6e23f..95010dc49 100644 --- a/libs/utils/include/celix_convert_utils.h +++ b/libs/utils/include/celix_convert_utils.h @@ -168,6 +168,9 @@ char* celix_utils_boolArrayListToString(const celix_array_list_t* list); * The expected format of the string is a "," separated list of strings. Whitespace is preserved. * String entries are copied and the returned list will be configured to call free when entries are removed. * + * The escaped character is "\" and can be used to escape "," and "\" characters. + * E.g. "a,b\,\\,c" will be converted to "a", "b,\" and "c". + * * @param[in] val The string to convert. * @param[in] defaultValue The default value if the string is not a valid "," separated list of strings. * Note that the defaultValue is copied if the string is not a valid list of string entries diff --git a/libs/utils/src/celix_convert_utils.c b/libs/utils/src/celix_convert_utils.c index b34844446..a51e2767f 100644 --- a/libs/utils/src/celix_convert_utils.c +++ b/libs/utils/src/celix_convert_utils.c @@ -27,9 +27,11 @@ #include "celix_array_list.h" #include "celix_err.h" -#include "celix_stdio_cleanup.h" #include "celix_utils.h" +#define ESCAPE_CHAR '\\' +#define SEPARATOR_CHAR ',' + static bool celix_utils_isEndptrEndOfStringOrOnlyContainsWhitespaces(const char* endptr) { bool result = false; if (endptr != NULL) { @@ -116,8 +118,6 @@ celix_utils_convertStringToVersion(const char* val, const celix_version_t* defau if (!val && defaultValue) { *version = celix_version_copy(defaultValue); return *version ? CELIX_ILLEGAL_ARGUMENT : CELIX_ENOMEM; - } else if (!val) { - return CELIX_ILLEGAL_ARGUMENT; } celix_status_t status = celix_version_parse(val, version); @@ -158,23 +158,50 @@ static celix_status_t celix_utils_convertStringToArrayList(const char* val, return CELIX_ENOMEM; } - char buf[256]; - char* valCopy = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s", val); - if (!valCopy) { - return CELIX_ENOMEM; - } - + char* buf = NULL; + size_t bufSize = 0; + FILE* entryStream = NULL; celix_status_t status = CELIX_SUCCESS; - char* savePtr = NULL; - char* token = strtok_r(valCopy, ",", &savePtr); - while (token != NULL) { - status = addEntry(result, token); - if (status != CELIX_SUCCESS) { - break; + size_t max = strlen(val); + for (size_t i = 0; i <= max; ++i) { + if (!entryStream) { + entryStream = open_memstream(&buf, &bufSize); + if (!entryStream) { + return CELIX_ENOMEM; + } + } + if (val[i] == ESCAPE_CHAR) { + // escape character, next char must be escapeChar or separatorChar + if (i + 1 < max && (val[i + 1] == ESCAPE_CHAR || val[i + 1] == SEPARATOR_CHAR)) { + // write escaped char + i += 1; + int rc = fputc(val[i], entryStream); + if (rc == EOF) { + return CELIX_ENOMEM; + } + continue; + } else { + // invalid escape (ending with escapeChar or followed by an invalid char) + status = CELIX_ILLEGAL_ARGUMENT; + break; + } + } else if (val[i] == SEPARATOR_CHAR || val[i] == '\0') { + //end of entry + fclose(entryStream); + entryStream = NULL; + status = addEntry(result, buf); + if (status == CELIX_ENOMEM) { + return status; + } + } else { + //normal char + int rc = fputc(val[i], entryStream); + if (rc == EOF) { + return CELIX_ENOMEM; + } } - token = strtok_r(NULL, ",", &savePtr); } - celix_utils_freeStringIfNotEqual(buf, valCopy); + if (status == CELIX_SUCCESS) { *list = celix_steal_ptr(result); @@ -220,7 +247,7 @@ celix_status_t celix_utils_convertStringToDoubleArrayList(const char* val, celix_status_t celix_utils_addBoolEntry(celix_array_list_t* list, const char* entry) { bool converted; - bool b = celix_utils_convertStringToBool(entry, 0.0, &converted); + bool b = celix_utils_convertStringToBool(entry, true, &converted); if (!converted) { return CELIX_ILLEGAL_ARGUMENT; } @@ -289,7 +316,7 @@ static char* celix_utils_arrayListToString(const celix_array_list_t* list, celix_array_list_entry_t entry = celix_arrayList_getEntry(list, i); int rc = printCb(stream, &entry); if (rc >= 0 && i < size - 1) { - rc = fputs(", ", stream); + rc = fputs(",", stream); } if (rc < 0) { celix_err_push("Cannot print to stream"); @@ -327,7 +354,21 @@ char* celix_utils_boolArrayListToString(const celix_array_list_t* list) { } static int celix_utils_printStrEntry(FILE* stream, const celix_array_list_entry_t* entry) { - return fprintf(stream, "%s", (const char*)entry->voidPtrVal); + const char* str = entry->strVal; + int rc = 0; + for (int i = 0; str[i] != '\0'; ++i) { + if (str[i] == ESCAPE_CHAR || str[i] == SEPARATOR_CHAR) { + //both escape and separator char need to be escaped + rc = fputc(ESCAPE_CHAR, stream); + } + if (rc != EOF) { + rc = fputc(str[i], stream); + } + if (rc == EOF) { + break; + } + } + return rc; } char* celix_utils_stringArrayListToString(const celix_array_list_t* list) {