From 3acdb746e55738f45134a6412602f66fc9b80a39 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 1 Apr 2024 20:28:02 +0200 Subject: [PATCH 01/25] gh-685: Initial properties save/load setup --- libs/utils/CMakeLists.txt | 1 + libs/utils/gtest/CMakeLists.txt | 1 + .../src/PropertiesSerializationTestSuite.cc | 64 +++++++++++++++++++ libs/utils/include/celix_properties.h | 6 ++ libs/utils/src/properties_serialization.c | 35 ++++++++++ 5 files changed, 107 insertions(+) create mode 100644 libs/utils/gtest/src/PropertiesSerializationTestSuite.cc create mode 100644 libs/utils/src/properties_serialization.c diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index e1b498359..064c9683f 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -29,6 +29,7 @@ if (UTILS) src/version.c src/version_range.c src/properties.c + src/properties_serialization.c src/utils.c src/filter.c src/celix_log_level.c diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt index d04f9e30c..62c1ac0c1 100644 --- a/libs/utils/gtest/CMakeLists.txt +++ b/libs/utils/gtest/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(test_utils src/CelixUtilsTestSuite.cc src/ConvertUtilsTestSuite.cc src/PropertiesTestSuite.cc + src/PropertiesSerializationTestSuite.cc src/VersionTestSuite.cc src/ErrTestSuite.cc src/ThreadsTestSuite.cc diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc new file mode 100644 index 000000000..d3bf6aa06 --- /dev/null +++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix_err.h" +#include "celix_properties.h" + +using ::testing::MatchesRegex; + +class PropertiesSerializationTestSuite : public ::testing::Test { + public: + PropertiesSerializationTestSuite() { celix_err_resetErrors(); } +}; + +TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { + //Given an empty properties object + celix_autoptr(celix_properties_t) props = celix_properties_create(); + + //And an in-memory stream + char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the stream contains an empty JSON object + fclose(stream); + EXPECT_STREQ("{}", buf); +} + +TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { + //Given an empty JSON object + const char* json = "{}"; + FILE* stream = fmemopen((void*)json, strlen(json), "r"); + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the properties object is empty + EXPECT_EQ(0, celix_properties_size(props)); + + fclose(stream); +} diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index 39e87c660..5903ecaa3 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -180,6 +180,12 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro const char* file, const char* header); +//TODO doc +CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream); + +//TODO doc +CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out); + /** * @brief Get the entry for a given key in a property set. * diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_serialization.c new file mode 100644 index 000000000..4ebda01e9 --- /dev/null +++ b/libs/utils/src/properties_serialization.c @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "celix_properties.h" + +celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream) { + celix_status_t status = CELIX_SUCCESS; + (void)properties; + fprintf(stream, "{}"); + return status; +} + +celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) { + celix_status_t status = CELIX_SUCCESS; + (void)stream; + celix_autoptr(celix_properties_t) props = celix_properties_create(); + *out = celix_steal_ptr(props); + return status; +} \ No newline at end of file From 9511610bb2c7379570c1bd3a9705202c0723a865 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Tue, 2 Apr 2024 20:25:09 +0200 Subject: [PATCH 02/25] gh-685: Add jansson dep for simple properties save/load --- libs/utils/CMakeLists.txt | 3 +- .../src/PropertiesSerializationTestSuite.cc | 70 ++++++++++- libs/utils/src/properties.c | 2 +- libs/utils/src/properties_serialization.c | 112 +++++++++++++++++- 4 files changed, 180 insertions(+), 7 deletions(-) diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index 064c9683f..4180b48be 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -18,6 +18,7 @@ celix_subproject(UTILS "Option to enable building the Utilities library" ON) if (UTILS) find_package(libzip REQUIRED) + find_package(jansson REQUIRED) #TODO add jansson dep info to build (conan) and documentation info set(MEMSTREAM_SOURCES ) set(MEMSTREAM_INCLUDES ) @@ -42,7 +43,7 @@ if (UTILS) src/celix_cleanup.c ${MEMSTREAM_SOURCES} ) - set(UTILS_PRIVATE_DEPS libzip::zip) + set(UTILS_PRIVATE_DEPS libzip::zip jansson::jansson) set(UTILS_PUBLIC_DEPS) add_library(utils SHARED ${UTILS_SRC}) diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc index d3bf6aa06..99cbeceb0 100644 --- a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc @@ -18,9 +18,11 @@ */ #include +#include #include "celix_err.h" #include "celix_properties.h" +#include "celix_stdlib_cleanup.h" using ::testing::MatchesRegex; @@ -34,7 +36,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { celix_autoptr(celix_properties_t) props = celix_properties_create(); //And an in-memory stream - char* buf = nullptr; + celix_autofree char* buf = nullptr; size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); @@ -62,3 +64,69 @@ TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { fclose(stream); } + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingelValuesTest) { + //Given a properties object with single values + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1", "value1"); + celix_properties_set(props, "key2", "value2"); + celix_properties_setLong(props, "key3", 3); + celix_properties_setDouble(props, "key4", 4.0); + celix_properties_setBool(props, "key5", true); + celix_properties_assignVersion(props, "key6", celix_version_create(1, 2, 3, "qualifier")); + + //And an in-memory stream + celix_autofree char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the stream contains the JSON representation snippets of the properties + fclose(stream); + EXPECT_NE(nullptr, strstr(buf, R"("key1":"value1")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key2":"value2")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key3":3)")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key4":4.0)")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key5":true)")) << "JSON: " << buf; + + //TODO how are versions serialized? A string representation is needed to reconstruct the version from JSON + EXPECT_NE(nullptr, strstr(buf, R"("key6":"celix_version<1.2.3.qualifier>")")) << "JSON: " << buf; + + //And the buf is a valid JSON object + json_error_t error; + json_t* root = json_loads(buf, 0, &error); + EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; + json_decref(root); +} + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { + //Given a properties object with array list values + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_array_list_t* list1 = celix_arrayList_createStringArray(); + celix_arrayList_addString(list1, "value1"); + celix_arrayList_addString(list1, "value2"); + celix_properties_assignArrayList(props, "key1", list1); + //TODO long, double, bool, version + + //And an in-memory stream + celix_autofree char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the stream contains the JSON representation snippets of the properties + fclose(stream); + EXPECT_NE(nullptr, strstr(buf, R"("key1":["value1","value2"])")) << "JSON: " << buf; + + //And the buf is a valid JSON object + json_error_t error; + json_t* root = json_loads(buf, 0, &error); + EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; + json_decref(root); +} diff --git a/libs/utils/src/properties.c b/libs/utils/src/properties.c index c51001127..024d849db 100644 --- a/libs/utils/src/properties.c +++ b/libs/utils/src/properties.c @@ -645,7 +645,7 @@ const celix_properties_entry_t* celix_properties_getEntry(const celix_properties return entry; } -static const bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry, +static bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry, celix_array_list_element_type_t elType) { return entry != NULL && entry->valueType == CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST && celix_arrayList_getElementType(entry->typed.arrayValue) == elType; diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_serialization.c index 4ebda01e9..d52fa6ef4 100644 --- a/libs/utils/src/properties_serialization.c +++ b/libs/utils/src/properties_serialization.c @@ -19,11 +19,115 @@ #include "celix_properties.h" +#include "celix_err.h" +#include "celix_stdlib_cleanup.h" + +#include + +//TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection + +static json_t* celix_properties_versionToJson(const celix_version_t *version) { + celix_autofree char* versionStr = celix_version_toString(version); // TODO error handling + return json_sprintf("celix_version<%s>", versionStr); +} + +static json_t* celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType, + celix_array_list_entry_t entry) { + switch (elType) { + case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING: + return json_string(entry.stringVal); + case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG: + return json_integer(entry.longVal); + case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE: + return json_real(entry.doubleVal); + case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL: + return json_boolean(entry.boolVal); + case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: + celix_properties_versionToJson(entry.versionVal); + default: + // LCOV_EXCL_START + celix_err_pushf("Unexpected array list element type %d", elType); + return NULL; + // LCOV_EXCL_STOP + } +} + +static json_t* celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry) { + json_t* array = json_array(); + if (!array) { + celix_err_push("Failed to create json array"); + return NULL; + } + + for (int i = 0; i < celix_arrayList_size(entry->typed.arrayValue); ++i) { + celix_array_list_entry_t arrayEntry = celix_arrayList_getEntry(entry->typed.arrayValue, i); + celix_array_list_element_type_t elType = celix_arrayList_getElementType(entry->typed.arrayValue); + json_t* jsonValue = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry); + if (!jsonValue) { + celix_err_push("Failed to create json string"); + json_decref(array); + return NULL; + } + int rc = json_array_append_new(array, jsonValue); + if (rc != 0) { + celix_err_push("Failed to append json string to array"); + json_decref(array); + return NULL; + } + } + return array; +} + +static json_t* celix_properties_entryValueToJson(const celix_properties_entry_t* entry) { + switch (entry->valueType) { + case CELIX_PROPERTIES_VALUE_TYPE_STRING: + return json_string(entry->value); + case CELIX_PROPERTIES_VALUE_TYPE_LONG: + return json_integer(entry->typed.longValue); + case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE: + return json_real(entry->typed.doubleValue); + case CELIX_PROPERTIES_VALUE_TYPE_BOOL: + return json_boolean(entry->typed.boolValue); + case CELIX_PROPERTIES_VALUE_TYPE_VERSION: + return celix_properties_versionToJson(entry->typed.versionValue); + case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST: + return celix_properties_arrayEntryValueToJson(entry); + default: + //LCOV_EXCL_START + celix_err_pushf("Unexpected properties entry type %d", entry->valueType);\ + return NULL; + //LCOV_EXCL_STOP + } +} + celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream) { - celix_status_t status = CELIX_SUCCESS; - (void)properties; - fprintf(stream, "{}"); - return status; + json_t* root = json_object(); + if (!root) { + celix_err_push("Failed to create json object"); + } + + CELIX_PROPERTIES_ITERATE(properties, iter) { + const char* key = iter.key; + json_t* value = celix_properties_entryValueToJson(&iter.entry); + if (!value) { + json_decref(root); + return CELIX_ENOMEM; //TODO improve error + } + int rc = json_object_set_new(root, key, value); + if (rc != 0) { + celix_err_push("Failed to set json object"); + json_decref(root); + return CELIX_ENOMEM; //TODO improve error + } + } + + int rc = json_dumpf(root, stream, JSON_COMPACT); //TODO make celix properties flags for COMPACT and INDENT and maybe other json flags + json_decref(root); + if (rc != 0) { + celix_err_push("Failed to dump json object"); + return CELIX_ENOMEM; //TODO improve error + } + return CELIX_SUCCESS; } celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) { From de0725649450bcbcfbc8145d652b7fb0ea6f0716 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 8 Apr 2024 23:03:03 +0200 Subject: [PATCH 03/25] gh-685: Add json prop loading for primitive and initial arr. --- .../src/PropertiesSerializationTestSuite.cc | 311 ++++++++++++- libs/utils/src/properties_serialization.c | 420 +++++++++++++++--- 2 files changed, 653 insertions(+), 78 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc index 99cbeceb0..a5d6a1df7 100644 --- a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc @@ -17,6 +17,7 @@ * under the License. */ +#include #include #include @@ -49,23 +50,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { EXPECT_STREQ("{}", buf); } -TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { - //Given an empty JSON object - const char* json = "{}"; - FILE* stream = fmemopen((void*)json, strlen(json), "r"); - - //When loading the properties from the stream - celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_loadFromStream(stream, &props); - EXPECT_EQ(CELIX_SUCCESS, status); - - //Then the properties object is empty - EXPECT_EQ(0, celix_properties_size(props)); - - fclose(stream); -} - -TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingelValuesTest) { +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { //Given a properties object with single values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); @@ -102,14 +87,115 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingelValuesTest) { json_decref(root); } +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) { + //Given a NAN, INF and -INF value + auto keys = {"NAN", "INF", "-INF"}; + for (const auto& key : keys) { + //For every value + + //Given a properties object with a NAN, INF or -INF value + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_setDouble(props, key, strtod(key, nullptr)); + + //And an in-memory stream + celix_autofree char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + //Then saving the properties to the stream fails, because JSON does not support NAN, INF and -INF + celix_err_resetErrors(); + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And an error msg is added to celix_err + EXPECT_EQ(1, celix_err_getErrorCount()); + } +} + + TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { - //Given a properties object with array list values + // Given a properties object with array list values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_array_list_t* list1 = celix_arrayList_createStringArray(); celix_arrayList_addString(list1, "value1"); celix_arrayList_addString(list1, "value2"); celix_properties_assignArrayList(props, "key1", list1); - //TODO long, double, bool, version + celix_array_list_t* list2 = celix_arrayList_createLongArray(); + celix_arrayList_addLong(list2, 1); + celix_arrayList_addLong(list2, 2); + celix_properties_assignArrayList(props, "key2", list2); + celix_array_list_t* list3 = celix_arrayList_createDoubleArray(); + celix_arrayList_addDouble(list3, 1.0); + celix_arrayList_addDouble(list3, 2.0); + celix_properties_assignArrayList(props, "key3", list3); + celix_array_list_t* list4 = celix_arrayList_createBoolArray(); + celix_arrayList_addBool(list4, true); + celix_arrayList_addBool(list4, false); + celix_properties_assignArrayList(props, "key4", list4); + celix_array_list_t* list5 = celix_arrayList_createVersionArray(); + celix_arrayList_addVersion(list5, celix_version_create(1, 2, 3, "qualifier")); + celix_arrayList_addVersion(list5, celix_version_create(4, 5, 6, "qualifier")); + celix_properties_assignArrayList(props, "key5", list5); + + // And an in-memory stream + celix_autofree char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + // When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + // Then the stream contains the JSON representation snippets of the properties + fclose(stream); + EXPECT_NE(nullptr, strstr(buf, R"("key1":["value1","value2"])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key2":[1,2])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key3":[1.0,2.0])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key4":[true,false])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key5":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"])")) + << "JSON: " << buf; + + // And the buf is a valid JSON object + json_error_t error; + json_t* root = json_loads(buf, 0, &error); + EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; + json_decref(root); +} + + +TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { + //Given a properties object with an empty array list of with el types string, long, double, bool, version + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_assignArrayList(props, "key1", celix_arrayList_createStringArray()); + celix_properties_assignArrayList(props, "key2", celix_arrayList_createLongArray()); + celix_properties_assignArrayList(props, "key3", celix_arrayList_createDoubleArray()); + celix_properties_assignArrayList(props, "key4", celix_arrayList_createBoolArray()); + celix_properties_assignArrayList(props, "key5", celix_arrayList_createVersionArray()); + EXPECT_EQ(5, celix_properties_size(props)); + + //And an in-memory stream + celix_autofree char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the stream contains an empty JSON object, because empty arrays are treated as unset + fclose(stream); + EXPECT_STREQ("{}", buf); +} + +TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { + //Given a properties object with jpath keys + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1", "value1"); + celix_properties_set(props, "key2", "value2"); + celix_properties_set(props, "object1/key3", "value3"); + celix_properties_set(props, "object1/key4", "value4"); + celix_properties_set(props, "object2/key5", "value5"); + celix_properties_set(props, "object3/object4/key6", "value6"); //And an in-memory stream celix_autofree char* buf = nullptr; @@ -122,7 +208,44 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { //Then the stream contains the JSON representation snippets of the properties fclose(stream); - EXPECT_NE(nullptr, strstr(buf, R"("key1":["value1","value2"])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key1":"value1")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key2":"value2")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("object1":{"key3":"value3","key4":"value4"})")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("object2":{"key5":"value5"})")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("object3":{"object4":{"key6":"value6"}})")) << "JSON: " << buf; + + //And the buf is a valid JSON object + json_error_t error; + json_t* root = json_loads(buf, 0, &error); + EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; + json_decref(root); +} + +TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { + //Given a properties object with jpath keys that collide + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1/key2/key3", "value1"); + celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2" + celix_properties_set(props, "key4/key5/key6", "value3"); + celix_properties_set(props, "key4/key5/key6/key7", "value4"); //collision with field "key3/key4/key5" + + //And an in-memory stream + celix_autofree char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the stream contains the JSON representation snippets of the properties + fclose(stream); + EXPECT_NE(nullptr, strstr(buf, R"("key1":{"key2":{"key3":"value1"}})")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key1/key2":"value2")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key4/key5/key6":"value3")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key4":{"key5":{"key6":{"key7":"value4"}}})")) << "JSON: " << buf; + //Note whether "key1/key2/key3" or "key1/key2" is serializer first depends on the hash order of the keys, + //so this test can change if the string hash map implementation changes. //And the buf is a valid JSON object json_error_t error; @@ -130,3 +253,151 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; json_decref(root); } + +TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { + //Given an empty JSON object + const char* json = "{}"; + FILE* stream = fmemopen((void*)json, strlen(json), "r"); + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the properties object is empty + EXPECT_EQ(0, celix_properties_size(props)); + + fclose(stream); +} + +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { + //Given a JSON object with single values for types string, long, double, bool and version + const char* jsonInput = R"({ + "strKey":"strValue", + "longKey":42, + "doubleKey":2.0, + "boolKey":true, + "versionKey":"celix_version<1.2.3.qualifier>" + })"; + + //And a stream with the JSON object + FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); + + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the properties object contains the single values + EXPECT_EQ(5, celix_properties_size(props)); + EXPECT_STREQ("strValue", celix_properties_getString(props, "strKey")); + EXPECT_EQ(42, celix_properties_getLong(props, "longKey", -1)); + EXPECT_DOUBLE_EQ(2.0, celix_properties_getDouble(props, "doubleKey", NAN)); + EXPECT_TRUE(celix_properties_getBool(props, "boolKey", false)); + auto* v = celix_properties_getVersion(props, "versionKey"); + ASSERT_NE(nullptr, v); + celix_autofree char* vStr = celix_version_toString(v); + EXPECT_STREQ("1.2.3.qualifier", vStr); +} + +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) { + //Given a JSON object with array values for types string, long, double, bool and version + const char* jsonInput = R"({ + "strArr":["value1","value2"], + "intArr":[1,2], + "realArr":[1.0,2.0], + "boolArr":[true,false], + "versionArr":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"] + })"; + + //And a stream with the JSON object + FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the properties object contains the array values + EXPECT_EQ(5, celix_properties_size(props)); + + //And the string array is correctly loaded + auto* strArr = celix_properties_getArrayList(props, "strArr"); + ASSERT_NE(nullptr, strArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING, celix_arrayList_getElementType(strArr)); + EXPECT_EQ(2, celix_arrayList_size(strArr)); + EXPECT_STREQ("value1", celix_arrayList_getString(strArr, 0)); + EXPECT_STREQ("value2", celix_arrayList_getString(strArr, 1)); + + //And the long array is correctly loaded + auto* intArr = celix_properties_getArrayList(props, "intArr"); + ASSERT_NE(nullptr, intArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG, celix_arrayList_getElementType(intArr)); + EXPECT_EQ(2, celix_arrayList_size(intArr)); + EXPECT_EQ(1, celix_arrayList_getLong(intArr, 0)); + EXPECT_EQ(2, celix_arrayList_getLong(intArr, 1)); + + //And the double array is correctly loaded + auto* realArr = celix_properties_getArrayList(props, "realArr"); + ASSERT_NE(nullptr, realArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(realArr)); + EXPECT_EQ(2, celix_arrayList_size(realArr)); + EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(realArr, 0)); + EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(realArr, 1)); + + //And the bool array is correctly loaded + auto* boolArr = celix_properties_getArrayList(props, "boolArr"); + ASSERT_NE(nullptr, boolArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL, celix_arrayList_getElementType(boolArr)); + EXPECT_EQ(2, celix_arrayList_size(boolArr)); + EXPECT_TRUE(celix_arrayList_getBool(boolArr, 0)); + EXPECT_FALSE(celix_arrayList_getBool(boolArr, 1)); + + //And the version array is correctly loaded + auto* versionArr = celix_properties_getArrayList(props, "versionArr"); + ASSERT_NE(nullptr, versionArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION, celix_arrayList_getElementType(versionArr)); + EXPECT_EQ(2, celix_arrayList_size(versionArr)); + auto* v1 = celix_arrayList_getVersion(versionArr, 0); + ASSERT_NE(nullptr, v1); + celix_autofree char* v1Str = celix_version_toString(v1); + EXPECT_STREQ("1.2.3.qualifier", v1Str); + auto* v2 = celix_arrayList_getVersion(versionArr, 1); + ASSERT_NE(nullptr, v2); + celix_autofree char* v2Str = celix_version_toString(v2); + EXPECT_STREQ("4.5.6.qualifier", v2Str); +} + +//TODO test with combination json_int and json_real, this should be promoted to double + +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) { + auto invalidInputs = { + R"({)", // invalid JSON (caught by jansson) + R"({"emptyArr":[]})", // Empty array, not supported + R"({"mixedArr":["string", true]})", // Mixed array, not supported + R"({"mixedArr":[1.9, 2]})", // Mixed array, TODO this should be supported + }; + for (auto& invalidInput: invalidInputs) { + //Given an invalid JSON object + FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + + //Then loading fails + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); + + fclose(stream); + } +} + +//TODO test deserialize null values +//TODO test serialize with empty array (treated as unset) +//TODO test with jpath subset keys and json serialization +//TODO test with key starting and ending with slash diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_serialization.c index d52fa6ef4..0b4c5f8b5 100644 --- a/libs/utils/src/properties_serialization.c +++ b/libs/utils/src/properties_serialization.c @@ -21,83 +21,194 @@ #include "celix_err.h" #include "celix_stdlib_cleanup.h" +#include "celix_utils.h" +#include #include +#include +#include -//TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection +static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, const json_t* jsonValue); -static json_t* celix_properties_versionToJson(const celix_version_t *version) { - celix_autofree char* versionStr = celix_version_toString(version); // TODO error handling - return json_sprintf("celix_version<%s>", versionStr); +// TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection + +static celix_status_t celix_properties_versionToJson(const celix_version_t* version, json_t** out) { + celix_autofree char* versionStr = celix_version_toString(version); + if (!versionStr) { + celix_err_push("Failed to create version string"); + return CELIX_ENOMEM; + } + *out = json_sprintf("celix_version<%s>", versionStr); + if (!*out) { + celix_err_push("Failed to create json string"); + return CELIX_ENOMEM; + } + return CELIX_SUCCESS; } -static json_t* celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType, - celix_array_list_entry_t entry) { +static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType, + celix_array_list_entry_t entry, + json_t** out) { + *out = NULL; switch (elType) { case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING: - return json_string(entry.stringVal); + *out = json_string(entry.stringVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG: - return json_integer(entry.longVal); + *out = json_integer(entry.longVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE: - return json_real(entry.doubleVal); + *out = json_real(entry.doubleVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL: - return json_boolean(entry.boolVal); + *out = json_boolean(entry.boolVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: - celix_properties_versionToJson(entry.versionVal); + return celix_properties_versionToJson(entry.versionVal, out); default: // LCOV_EXCL_START celix_err_pushf("Unexpected array list element type %d", elType); - return NULL; + return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } + if (!*out) { + celix_err_push("Failed to create json value"); + return CELIX_ENOMEM; + } + return CELIX_SUCCESS; } -static json_t* celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry) { +static celix_status_t celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry, json_t** out) { + *out = NULL; + if (celix_arrayList_size(entry->typed.arrayValue) == 0) { + return CELIX_SUCCESS; // empty array -> treat as unset property + } + json_t* array = json_array(); if (!array) { celix_err_push("Failed to create json array"); - return NULL; + return CELIX_ENOMEM; } for (int i = 0; i < celix_arrayList_size(entry->typed.arrayValue); ++i) { celix_array_list_entry_t arrayEntry = celix_arrayList_getEntry(entry->typed.arrayValue, i); celix_array_list_element_type_t elType = celix_arrayList_getElementType(entry->typed.arrayValue); - json_t* jsonValue = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry); - if (!jsonValue) { - celix_err_push("Failed to create json string"); - json_decref(array); - return NULL; - } - int rc = json_array_append_new(array, jsonValue); - if (rc != 0) { - celix_err_push("Failed to append json string to array"); + json_t* jsonValue; + celix_status_t status = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry, &jsonValue); + if (status != CELIX_SUCCESS) { json_decref(array); - return NULL; + return status; + } else if (!jsonValue) { + // ignore unset values + } else { + int rc = json_array_append_new(array, jsonValue); + if (rc != 0) { + celix_err_push("Failed to append json string to array"); + json_decref(array); + return CELIX_ENOMEM; + } } } - return array; + + *out = array; + return CELIX_SUCCESS; } -static json_t* celix_properties_entryValueToJson(const celix_properties_entry_t* entry) { +static celix_status_t celix_properties_entryValueToJson(const celix_properties_entry_t* entry, json_t** out) { + *out = NULL; switch (entry->valueType) { - case CELIX_PROPERTIES_VALUE_TYPE_STRING: - return json_string(entry->value); - case CELIX_PROPERTIES_VALUE_TYPE_LONG: - return json_integer(entry->typed.longValue); - case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE: - return json_real(entry->typed.doubleValue); - case CELIX_PROPERTIES_VALUE_TYPE_BOOL: - return json_boolean(entry->typed.boolValue); - case CELIX_PROPERTIES_VALUE_TYPE_VERSION: - return celix_properties_versionToJson(entry->typed.versionValue); - case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST: - return celix_properties_arrayEntryValueToJson(entry); - default: - //LCOV_EXCL_START - celix_err_pushf("Unexpected properties entry type %d", entry->valueType);\ - return NULL; - //LCOV_EXCL_STOP + case CELIX_PROPERTIES_VALUE_TYPE_STRING: + *out = json_string(entry->value); + break; + case CELIX_PROPERTIES_VALUE_TYPE_LONG: + *out = json_integer(entry->typed.longValue); + break; + case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE: + if (isnan(entry->typed.doubleValue) || isinf(entry->typed.doubleValue)) { + celix_err_pushf("Double NaN or Inf not supported in JSON."); + return CELIX_ILLEGAL_ARGUMENT; + } + *out = json_real(entry->typed.doubleValue); + break; + case CELIX_PROPERTIES_VALUE_TYPE_BOOL: + *out = json_boolean(entry->typed.boolValue); + break; + case CELIX_PROPERTIES_VALUE_TYPE_VERSION: + return celix_properties_versionToJson(entry->typed.versionValue, out); + case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST: + return celix_properties_arrayEntryValueToJson(entry, out); + default: + // LCOV_EXCL_START + celix_err_pushf("Unexpected properties entry type %d", entry->valueType); + return CELIX_ILLEGAL_ARGUMENT; + // LCOV_EXCL_STOP + } + + if (!*out) { + celix_err_push("Failed to create json value"); + return CELIX_ENOMEM; } + return CELIX_SUCCESS; +} + +static celix_status_t +celix_properties_addEntryToJson(const celix_properties_entry_t* entry, const char* key, json_t* root) { + json_t* jsonObj = root; + const char* subKey = key; + const char* slash = strstr(key, "/"); + while (slash) { + celix_autofree const char* name = strndup(subKey, slash - subKey); + if (!name) { + celix_err_push("Failed to create name string"); + return CELIX_ENOMEM; + } + json_t* subObj = json_object_get(jsonObj, name); + if (!subObj) { + subObj = json_object(); + if (!subObj) { + celix_err_push("Failed to create json object"); + return CELIX_ENOMEM; + } + int rc = json_object_set_new(jsonObj, name, subObj); + if (rc != 0) { + celix_err_push("Failed to set json object"); + return CELIX_ENOMEM; + } + } else if (!json_is_object(subObj)) { + // subObj is not an object, so obj cannot be added -> adding obj flat + jsonObj = root; + subKey = key; + break; + } + + jsonObj = subObj; + subKey = slash + 1; + slash = strstr(subKey, "/"); + + json_t* field = json_object_get(jsonObj, subKey); + if (field) { + // field already exists, so adding obj flat + jsonObj = root; + subKey = key; + break; + } + } + + json_t* value; + celix_status_t status = celix_properties_entryValueToJson(entry, &value); + if (status != CELIX_SUCCESS) { + return status; + } else if (!value) { + // ignore unset values + } else { + int rc = json_object_set_new(jsonObj, subKey, value); + if (rc != 0) { + celix_err_push("Failed to set json object"); + return CELIX_ENOMEM; + } + } + + return CELIX_SUCCESS; } celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream) { @@ -107,33 +218,226 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie } CELIX_PROPERTIES_ITERATE(properties, iter) { - const char* key = iter.key; - json_t* value = celix_properties_entryValueToJson(&iter.entry); - if (!value) { - json_decref(root); - return CELIX_ENOMEM; //TODO improve error - } - int rc = json_object_set_new(root, key, value); - if (rc != 0) { - celix_err_push("Failed to set json object"); + celix_status_t status = celix_properties_addEntryToJson(&iter.entry, iter.key, root); + if (status != CELIX_SUCCESS) { json_decref(root); - return CELIX_ENOMEM; //TODO improve error + return status; } } - int rc = json_dumpf(root, stream, JSON_COMPACT); //TODO make celix properties flags for COMPACT and INDENT and maybe other json flags + int rc = + json_dumpf(root, + stream, + JSON_COMPACT); // TODO make celix properties flags for COMPACT and INDENT and maybe other json flags json_decref(root); if (rc != 0) { celix_err_push("Failed to dump json object"); - return CELIX_ENOMEM; //TODO improve error + return CELIX_ENOMEM; // TODO improve error } return CELIX_SUCCESS; } -celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) { +static celix_version_t* celix_properties_parseVersion(const char* value) { + // precondition: value is a valid version string (14 chars prefix and 1 char suffix) + celix_version_t* version = NULL; + char buf[32]; + char* corrected = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 15, value + 14); + if (!corrected) { + celix_err_push("Failed to create corrected version string"); + return NULL; + } + celix_status_t status = celix_version_parse(corrected, &version); + celix_utils_freeStringIfNotEqual(buf, corrected); + if (status != CELIX_SUCCESS) { + celix_err_push("Failed to parse version string"); + return NULL; + } + return version; +} + +static bool celix_properties_isVersionString(const char* value) { + return strncmp(value, "celix_version<", 14) == 0 && value[strlen(value) - 1] == '>'; +} + +/** + * @brief Determine the array list element type based on the json value. + * + * If the array is empty or of a mixed type, the element type cannot be determined and a CELIX_ILLEGAL_ARGUMENT is + * returned. + * + * @param[in] value The json value. + * @param[out] out The array list element type. + * @return CELIX_SUCCESS if the array list element type could be determined or CELIX_ILLEGAL_ARGUMENT if the array + * type could not be determined. + */ +static celix_status_t celix_properties_determineArrayType(const json_t* jsonArray, + celix_array_list_element_type_t* out) { + size_t size = json_array_size(jsonArray); + if (size == 0) { + celix_err_push("Empty array"); + return CELIX_ILLEGAL_ARGUMENT; + } + + json_t* value; + int index; + json_type type = JSON_NULL; + bool versionType = false; + json_array_foreach(jsonArray, index, value) { + if (index == 0) { + type = json_typeof(value); + if (type == JSON_STRING && celix_properties_isVersionString(json_string_value(value))) { + versionType = true; + } + } else if ((type == JSON_TRUE || type == JSON_FALSE) && + (json_typeof(value) == JSON_TRUE || json_typeof(value) == JSON_FALSE)) { + // bool, ok. + continue; + } else if (type != json_typeof(value)) { + celix_err_push("Mixed types in array"); + return CELIX_ILLEGAL_ARGUMENT; + } else if (versionType) { + if (json_typeof(value) != JSON_STRING || !celix_properties_isVersionString(json_string_value(value))) { + celix_err_push("Mixed version and non-version strings in array"); + return CELIX_ILLEGAL_ARGUMENT; + } + } + } + + switch (type) { + case JSON_STRING: + *out = versionType ? CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION : CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING; + break; + case JSON_INTEGER: + *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG; + break; + case JSON_REAL: + *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE; + break; + case JSON_TRUE: + case JSON_FALSE: + *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL; + break; + default: + celix_err_pushf("Unexpected json array type %d", type); + return CELIX_ILLEGAL_ARGUMENT; + } + + return CELIX_SUCCESS; +} + +static celix_status_t celix_properties_loadArray(celix_properties_t* props, const char* key, const json_t* jsonArray) { + celix_array_list_element_type_t elType; + celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType); + if (status != CELIX_SUCCESS) { + return status; + } + + celix_array_list_create_options_t opts = CELIX_EMPTY_ARRAY_LIST_CREATE_OPTIONS; + opts.elementType = elType; + celix_autoptr(celix_array_list_t) array = celix_arrayList_createWithOptions(&opts); + if (!array) { + return CELIX_ENOMEM; + } + + json_t* value; + int index; + json_array_foreach(jsonArray, index, value) { + switch (elType) { + case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING: + status = celix_arrayList_addString(array, json_string_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG: + status = celix_arrayList_addLong(array, (long)json_integer_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE: + status = celix_arrayList_addDouble(array, json_real_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL: + status = celix_arrayList_addBool(array, json_boolean_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: { + celix_version_t* v = celix_properties_parseVersion(json_string_value(value)); + if (!v) { + return CELIX_ILLEGAL_ARGUMENT; + } + status = celix_arrayList_addVersion(array, v); + break; + } + default: + // LCOV_EXCL_START + celix_err_pushf("Unexpected array list element type %d", elType); + return CELIX_ILLEGAL_ARGUMENT; + // LCOV_EXCL_STOP + } + if (status != CELIX_SUCCESS) { + return status; + } + } + return celix_properties_assignArrayList(props, key, celix_steal_ptr(array)); +} + +static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, const json_t* jsonValue) { celix_status_t status = CELIX_SUCCESS; - (void)stream; + if (json_is_string(jsonValue) && celix_properties_isVersionString(json_string_value(jsonValue))) { + celix_version_t* version = celix_properties_parseVersion(json_string_value(jsonValue)); + if (!version) { + return CELIX_ILLEGAL_ARGUMENT; + } + status = celix_properties_setVersion(props, key, version); + } else if (json_is_string(jsonValue)) { + status = celix_properties_setString(props, key, json_string_value(jsonValue)); + } else if (json_is_integer(jsonValue)) { + status = celix_properties_setLong(props, key, json_integer_value(jsonValue)); + } else if (json_is_real(jsonValue)) { + status = celix_properties_setDouble(props, key, json_real_value(jsonValue)); + } else if (json_is_boolean(jsonValue)) { + status = celix_properties_setBool(props, key, json_boolean_value(jsonValue)); + } else if (json_is_object(jsonValue)) { + // TODO + status = CELIX_ILLEGAL_ARGUMENT; + } else if (json_is_array(jsonValue)) { + status = celix_properties_loadArray(props, key, jsonValue); + } else { + // LCOV_EXCL_START + celix_err_pushf("Unexpected json value type"); + return CELIX_ILLEGAL_ARGUMENT; + // LCOV_EXCL_STOP + } + return status; +} + +static celix_status_t celix_properties_loadFromJson(json_t* obj, celix_properties_t** out) { + assert(obj != NULL && json_is_object(obj)); celix_autoptr(celix_properties_t) props = celix_properties_create(); + if (!props) { + return CELIX_ENOMEM; + } + + // add loop (obj=root, prefix="" and extend prefix when going into sub objects) + const char* key; + json_t* value; + json_object_foreach(obj, key, value) { + if (json_is_object(value)) { + // TODO + return CELIX_ILLEGAL_ARGUMENT; + } + celix_status_t status = celix_properties_loadValue(props, key, value); + if (status != CELIX_SUCCESS) { + return status; + } + } + *out = celix_steal_ptr(props); - return status; + return CELIX_SUCCESS; +} + +celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) { + json_error_t jsonError; + json_t* root = json_loadf(stream, 0, &jsonError); + if (!root) { + celix_err_pushf("Failed to parse json: %s", jsonError.text); + return CELIX_ILLEGAL_ARGUMENT; + } + + return celix_properties_loadFromJson(root, out); } \ No newline at end of file From 04bccbd3741e427b52790d4bfc3659f9e30c1583 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Tue, 9 Apr 2024 20:03:50 +0200 Subject: [PATCH 04/25] gh-685: Add support for nested obj properties decoding --- .../src/PropertiesSerializationTestSuite.cc | 246 ++++++++++++++---- libs/utils/include/celix_properties.h | 21 +- libs/utils/src/properties_serialization.c | 72 +++-- 3 files changed, 262 insertions(+), 77 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc index a5d6a1df7..4aa2469d5 100644 --- a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc @@ -32,7 +32,7 @@ class PropertiesSerializationTestSuite : public ::testing::Test { PropertiesSerializationTestSuite() { celix_err_resetErrors(); } }; -TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { +TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) { //Given an empty properties object celix_autoptr(celix_properties_t) props = celix_properties_create(); @@ -41,16 +41,16 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream); - EXPECT_EQ(CELIX_SUCCESS, status); + //When encoding the properties to the stream + auto status = celix_properties_encodeToStream(props, stream, 0); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains an empty JSON object fclose(stream); EXPECT_STREQ("{}", buf); } -TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { +TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) { //Given a properties object with single values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); @@ -65,9 +65,9 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream); - EXPECT_EQ(CELIX_SUCCESS, status); + //When encoding the properties to the stream + auto status = celix_properties_encodeToStream(props, stream, 0); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties fclose(stream); @@ -76,8 +76,6 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { EXPECT_NE(nullptr, strstr(buf, R"("key3":3)")) << "JSON: " << buf; EXPECT_NE(nullptr, strstr(buf, R"("key4":4.0)")) << "JSON: " << buf; EXPECT_NE(nullptr, strstr(buf, R"("key5":true)")) << "JSON: " << buf; - - //TODO how are versions serialized? A string representation is needed to reconstruct the version from JSON EXPECT_NE(nullptr, strstr(buf, R"("key6":"celix_version<1.2.3.qualifier>")")) << "JSON: " << buf; //And the buf is a valid JSON object @@ -87,7 +85,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { json_decref(root); } -TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) { +TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithNaNAndInfValuesTest) { //Given a NAN, INF and -INF value auto keys = {"NAN", "INF", "-INF"}; for (const auto& key : keys) { @@ -104,7 +102,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) //Then saving the properties to the stream fails, because JSON does not support NAN, INF and -INF celix_err_resetErrors(); - auto status = celix_properties_saveToStream(props, stream); + auto status = celix_properties_encodeToStream(props, stream, 0); EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); //And an error msg is added to celix_err @@ -113,7 +111,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) } -TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { +TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) { // Given a properties object with array list values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_array_list_t* list1 = celix_arrayList_createStringArray(); @@ -143,8 +141,8 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { FILE* stream = open_memstream(&buf, &bufLen); // When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream); - EXPECT_EQ(CELIX_SUCCESS, status); + auto status = celix_properties_encodeToStream(props, stream, 0); + ASSERT_EQ(CELIX_SUCCESS, status); // Then the stream contains the JSON representation snippets of the properties fclose(stream); @@ -163,7 +161,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { } -TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { +TEST_F(PropertiesSerializationTestSuite, EncodeEmptyArrayTest) { //Given a properties object with an empty array list of with el types string, long, double, bool, version celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_assignArrayList(props, "key1", celix_arrayList_createStringArray()); @@ -178,16 +176,16 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream); - EXPECT_EQ(CELIX_SUCCESS, status); + //When encoding the properties to the stream + auto status = celix_properties_encodeToStream(props, stream, 0); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains an empty JSON object, because empty arrays are treated as unset fclose(stream); EXPECT_STREQ("{}", buf); } -TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { +TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) { //Given a properties object with jpath keys celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); @@ -202,9 +200,9 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream); - EXPECT_EQ(CELIX_SUCCESS, status); + //When encoding the properties to the stream + auto status = celix_properties_encodeToStream(props, stream, 0); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties fclose(stream); @@ -221,7 +219,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { json_decref(root); } -TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { +TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysWithCollisionTest) { //Given a properties object with jpath keys that collide celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1/key2/key3", "value1"); @@ -234,9 +232,9 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream); - EXPECT_EQ(CELIX_SUCCESS, status); + //When encoding the properties to the stream + auto status = celix_properties_encodeToStream(props, stream, 0); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties fclose(stream); @@ -254,15 +252,15 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { json_decref(root); } -TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { +TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) { //Given an empty JSON object const char* json = "{}"; FILE* stream = fmemopen((void*)json, strlen(json), "r"); - //When loading the properties from the stream + //When decoding the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_loadFromStream(stream, &props); - EXPECT_EQ(CELIX_SUCCESS, status); + auto status = celix_properties_decodeFromStream(stream, 0, &props); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object is empty EXPECT_EQ(0, celix_properties_size(props)); @@ -270,7 +268,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { fclose(stream); } -TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) { //Given a JSON object with single values for types string, long, double, bool and version const char* jsonInput = R"({ "strKey":"strValue", @@ -283,11 +281,10 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { //And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - - //When loading the properties from the stream + //When decoding the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_loadFromStream(stream, &props); - EXPECT_EQ(CELIX_SUCCESS, status); + auto status = celix_properties_decodeFromStream(stream, 0, &props); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object contains the single values EXPECT_EQ(5, celix_properties_size(props)); @@ -301,26 +298,28 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { EXPECT_STREQ("1.2.3.qualifier", vStr); } -TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) { +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) { //Given a JSON object with array values for types string, long, double, bool and version const char* jsonInput = R"({ "strArr":["value1","value2"], "intArr":[1,2], "realArr":[1.0,2.0], "boolArr":[true,false], - "versionArr":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"] + "versionArr":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"], + "mixedRealAndIntArr1":[1,2.0,2,3.0], + "mixedRealAndIntArr2":[1.0,2,2.0,3] })"; //And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - //When loading the properties from the stream + //When decoding the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_loadFromStream(stream, &props); - EXPECT_EQ(CELIX_SUCCESS, status); + auto status = celix_properties_decodeFromStream(stream, 0, &props); + ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object contains the array values - EXPECT_EQ(5, celix_properties_size(props)); + EXPECT_EQ(7, celix_properties_size(props)); //And the string array is correctly loaded auto* strArr = celix_properties_getArrayList(props, "strArr"); @@ -367,24 +366,42 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) { ASSERT_NE(nullptr, v2); celix_autofree char* v2Str = celix_version_toString(v2); EXPECT_STREQ("4.5.6.qualifier", v2Str); -} -//TODO test with combination json_int and json_real, this should be promoted to double + //And the mixed json real and int arrays are correctly loaded as double arrays + auto* mixedRealAndIntArr1 = celix_properties_getArrayList(props, "mixedRealAndIntArr1"); + ASSERT_NE(nullptr, mixedRealAndIntArr1); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(mixedRealAndIntArr1)); + EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr1)); + EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 0)); + EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 1)); + EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 2)); + EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 3)); + + auto* mixedRealAndIntArr2 = celix_properties_getArrayList(props, "mixedRealAndIntArr2"); + ASSERT_NE(nullptr, mixedRealAndIntArr2); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(mixedRealAndIntArr2)); + EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr2)); + EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 0)); + EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 1)); + EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 2)); + EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 3)); +} -TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) { +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { auto invalidInputs = { R"({)", // invalid JSON (caught by jansson) - R"({"emptyArr":[]})", // Empty array, not supported + R"([])", // unsupported JSON (top level array not supported) + R"(42)", // invalid JSON (caught by jansson) R"({"mixedArr":["string", true]})", // Mixed array, not supported - R"({"mixedArr":[1.9, 2]})", // Mixed array, TODO this should be supported + R"({"key1":null})", // Null value, not supported }; for (auto& invalidInput: invalidInputs) { //Given an invalid JSON object FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); - //When loading the properties from the stream + //When decoding the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_loadFromStream(stream, &props); + auto status = celix_properties_decodeFromStream(stream, 0, &props); //Then loading fails EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -397,7 +414,132 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) { } } -//TODO test deserialize null values -//TODO test serialize with empty array (treated as unset) -//TODO test with jpath subset keys and json serialization +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithEmptyArrayTest) { + //Given a JSON object with an empty array + auto* emptyArrays = R"({"key1":[]})"; + + //And a stream with the JSON object + FILE* stream = fmemopen((void*)emptyArrays, strlen(emptyArrays), "r"); + + //When decoding the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_decodeFromStream(stream, 0, &props); + + //Then loading succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + //And the properties object is empty, because empty arrays are treated as unset + EXPECT_EQ(0, celix_properties_size(props)); +} + +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsTest) { + // Given a complex JSON object + const char* jsonInput = R"({ + "key1":"value1", + "key2":"value2", + "object1": { + "key3":"value3", + "key4":true + }, + "object2": { + "key5":5.0 + }, + "object3":{ + "object4":{ + "key6":6 + } + } + })"; + + // And a stream with the JSON object + FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); + + // When decoding the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_decodeFromStream(stream, 0, &props); + ASSERT_EQ(CELIX_SUCCESS, status); + + // Then the properties object contains the nested objects + EXPECT_EQ(6, celix_properties_size(props)); + EXPECT_STREQ("value1", celix_properties_getString(props, "key1")); + EXPECT_STREQ("value2", celix_properties_getString(props, "key2")); + EXPECT_STREQ("value3", celix_properties_getString(props, "object1/key3")); + EXPECT_EQ(true, celix_properties_getBool(props, "object1/key4", false)); + EXPECT_DOUBLE_EQ(5., celix_properties_getDouble(props, "object2/key5", 0.0)); + EXPECT_EQ(6, celix_properties_getLong(props, "object3/object4/key6", 0)); +} + +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPathCollisionTest) { + // Given a complex JSON object with jpath keys that collide + const char* jsonInput = R"({ + "object1": { + "object2": { + "key1":true + } + }, + "object1/object2/key1":6 + })"; + + // And a stream with the JSON object + FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); + + // When decoding the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_decodeFromStream(stream, 0, &props); + + // Then loading fails + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +//TODO +//TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabledTest) { +// auto invalidInputs = { +// R"({"mixedArr":["string", true]})", // Mixed array gives error on strict +// R"({"key1":null})", // Null value gives error on strict +// R"({"":"value"})", // "" key gives error on strict +// R"({"emptyArr":[]})", // Empty array gives error on strict +// R"({"key1":"val1", "key1:"val2"})", // Duplicate key gives error on strict +// }; +// +// for (auto& invalidInput: invalidInputs) { +// //Given an invalid JSON object +// FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); +// +// //When decoding the properties from the stream with an empty flags +// celix_autoptr(celix_properties_t) props = nullptr; +// auto status = celix_properties_decodeFromStream(stream, 0, &props); +// +// //Then decoding succeeds, because strict is disabled +// ASSERT_EQ(CELIX_SUCCESS, status); +// EXPECT_GE(celix_err_getErrorCount(), 0); +// +// //But the properties object is empty, because the invalid input is ignored +// EXPECT_EQ(0, celix_properties_size(props)); +// +// fclose(stream); +// } +// +// for (auto& invalidInput: invalidInputs) { +// //Given an invalid JSON object +// FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); +// +// //When decoding the properties from the stream with a strict flag +// celix_autoptr(celix_properties_t) props = nullptr; +// auto status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props); +// +// //Then decoding fails +// EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); +// +// //And at least one error message is added to celix_err +// EXPECT_GE(celix_err_getErrorCount(), 1); +// celix_err_printErrors(stderr, "Error: ", "\n"); +// +// fclose(stream); +// } +//} + //TODO test with key starting and ending with slash diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index 5903ecaa3..e501071c0 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -180,11 +180,28 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro const char* file, const char* header); +#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 +#define CELIX_PROPERTIES_ENCODE_SORT_KEYS 0x02 + +#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x02 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x04 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x04 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x08 +#define CELIX_PROPERTIES_DECODE_STRICT \ + (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | \ + CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | \ + CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) + //TODO doc -CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream); +CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, + FILE* stream, + int encodeFlags); //TODO doc -CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out); +CELIX_UTILS_EXPORT celix_status_t celix_properties_decodeFromStream(FILE* stream, + int decodeFlags, + celix_properties_t** out); /** * @brief Get the entry for a given key in a property set. diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_serialization.c index 0b4c5f8b5..a75c9f57c 100644 --- a/libs/utils/src/properties_serialization.c +++ b/libs/utils/src/properties_serialization.c @@ -28,7 +28,7 @@ #include #include -static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, const json_t* jsonValue); +static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, json_t* jsonValue); // TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection @@ -211,7 +211,7 @@ celix_properties_addEntryToJson(const celix_properties_entry_t* entry, const cha return CELIX_SUCCESS; } -celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream) { +celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) { json_t* root = json_object(); if (!root) { celix_err_push("Failed to create json object"); @@ -262,7 +262,7 @@ static bool celix_properties_isVersionString(const char* value) { /** * @brief Determine the array list element type based on the json value. * - * If the array is empty or of a mixed type, the element type cannot be determined and a CELIX_ILLEGAL_ARGUMENT is + * If the array is of a mixed type, the element type cannot be determined and a CELIX_ILLEGAL_ARGUMENT is * returned. * * @param[in] value The json value. @@ -273,10 +273,7 @@ static bool celix_properties_isVersionString(const char* value) { static celix_status_t celix_properties_determineArrayType(const json_t* jsonArray, celix_array_list_element_type_t* out) { size_t size = json_array_size(jsonArray); - if (size == 0) { - celix_err_push("Empty array"); - return CELIX_ILLEGAL_ARGUMENT; - } + assert(size > 0); //precondition: size > 0 json_t* value; int index; @@ -288,10 +285,16 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra if (type == JSON_STRING && celix_properties_isVersionString(json_string_value(value))) { versionType = true; } - } else if ((type == JSON_TRUE || type == JSON_FALSE) && - (json_typeof(value) == JSON_TRUE || json_typeof(value) == JSON_FALSE)) { + } else if ((type == JSON_TRUE || type == JSON_FALSE) && json_is_boolean(value)) { // bool, ok. continue; + } else if (type == JSON_INTEGER && json_typeof(value) == JSON_REAL) { + // mixed integer and real, ok but promote to real + type = JSON_REAL; + continue; + } else if (type == JSON_REAL && json_typeof(value) == JSON_INTEGER) { + // mixed real and integer, ok + continue; } else if (type != json_typeof(value)) { celix_err_push("Mixed types in array"); return CELIX_ILLEGAL_ARGUMENT; @@ -350,7 +353,7 @@ static celix_status_t celix_properties_loadArray(celix_properties_t* props, cons status = celix_arrayList_addLong(array, (long)json_integer_value(value)); break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE: - status = celix_arrayList_addDouble(array, json_real_value(value)); + status = celix_arrayList_addDouble(array, json_number_value(value)); break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL: status = celix_arrayList_addBool(array, json_boolean_value(value)); @@ -376,7 +379,12 @@ static celix_status_t celix_properties_loadArray(celix_properties_t* props, cons return celix_properties_assignArrayList(props, key, celix_steal_ptr(array)); } -static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, const json_t* jsonValue) { +static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, json_t* jsonValue) { + if (celix_properties_hasKey(props, key)) { + celix_err_pushf("Key `%s` already exists.", key); + return CELIX_ILLEGAL_ARGUMENT; + } + celix_status_t status = CELIX_SUCCESS; if (json_is_string(jsonValue) && celix_properties_isVersionString(json_string_value(jsonValue))) { celix_version_t* version = celix_properties_parseVersion(json_string_value(jsonValue)); @@ -393,34 +401,52 @@ static celix_status_t celix_properties_loadValue(celix_properties_t* props, cons } else if (json_is_boolean(jsonValue)) { status = celix_properties_setBool(props, key, json_boolean_value(jsonValue)); } else if (json_is_object(jsonValue)) { - // TODO - status = CELIX_ILLEGAL_ARGUMENT; + const char* fieldName; + json_t* fieldValue; + json_object_foreach(jsonValue, fieldName, fieldValue) { + celix_autofree char* subKey; + int rc = asprintf(&subKey, "%s/%s", key, fieldName); + if (rc < 0) { + celix_err_push("Failed to create sub key"); + return CELIX_ENOMEM; + } + status = celix_properties_loadValue(props, subKey, fieldValue); + if (status != CELIX_SUCCESS) { + return status; + } + } + return CELIX_SUCCESS; + } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) { + // empty array -> treat as unset property. silently ignore + return CELIX_SUCCESS; } else if (json_is_array(jsonValue)) { status = celix_properties_loadArray(props, key, jsonValue); + } else if (json_is_null(jsonValue)) { + celix_err_pushf("Unexpected null value for key `%s`", key); + return CELIX_ILLEGAL_ARGUMENT; } else { // LCOV_EXCL_START - celix_err_pushf("Unexpected json value type"); + celix_err_pushf("Unexpected json value type for key `%s`", key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } return status; } -static celix_status_t celix_properties_loadFromJson(json_t* obj, celix_properties_t** out) { - assert(obj != NULL && json_is_object(obj)); +static celix_status_t celix_properties_decodeFromJson(json_t* obj, celix_properties_t** out) { + if (!json_is_object(obj)) { + celix_err_push("Expected json object"); + return CELIX_ILLEGAL_ARGUMENT; + } + celix_autoptr(celix_properties_t) props = celix_properties_create(); if (!props) { return CELIX_ENOMEM; } - // add loop (obj=root, prefix="" and extend prefix when going into sub objects) const char* key; json_t* value; json_object_foreach(obj, key, value) { - if (json_is_object(value)) { - // TODO - return CELIX_ILLEGAL_ARGUMENT; - } celix_status_t status = celix_properties_loadValue(props, key, value); if (status != CELIX_SUCCESS) { return status; @@ -431,7 +457,7 @@ static celix_status_t celix_properties_loadFromJson(json_t* obj, celix_propertie return CELIX_SUCCESS; } -celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) { +celix_status_t celix_properties_decodeFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) { json_error_t jsonError; json_t* root = json_loadf(stream, 0, &jsonError); if (!root) { @@ -439,5 +465,5 @@ celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t* return CELIX_ILLEGAL_ARGUMENT; } - return celix_properties_loadFromJson(root, out); + return celix_properties_decodeFromJson(root, out); } \ No newline at end of file From 77cd6b37e7bae39663b20dee7c6fb31593a00c89 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Tue, 9 Apr 2024 20:05:33 +0200 Subject: [PATCH 05/25] gh-685: Rename prop serialization sources to prop encoding --- libs/utils/CMakeLists.txt | 2 +- libs/utils/gtest/CMakeLists.txt | 2 +- ...SerializationTestSuite.cc => PropertiesEncodingTestSuite.cc} | 0 .../src/{properties_serialization.c => properties_encoding.c} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename libs/utils/gtest/src/{PropertiesSerializationTestSuite.cc => PropertiesEncodingTestSuite.cc} (100%) rename libs/utils/src/{properties_serialization.c => properties_encoding.c} (100%) diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index 4180b48be..cec80f41d 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -30,7 +30,7 @@ if (UTILS) src/version.c src/version_range.c src/properties.c - src/properties_serialization.c + src/properties_encoding.c src/utils.c src/filter.c src/celix_log_level.c diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt index 62c1ac0c1..11c6ec00c 100644 --- a/libs/utils/gtest/CMakeLists.txt +++ b/libs/utils/gtest/CMakeLists.txt @@ -29,7 +29,7 @@ add_executable(test_utils src/CelixUtilsTestSuite.cc src/ConvertUtilsTestSuite.cc src/PropertiesTestSuite.cc - src/PropertiesSerializationTestSuite.cc + src/PropertiesEncodingTestSuite.cc src/VersionTestSuite.cc src/ErrTestSuite.cc src/ThreadsTestSuite.cc diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc similarity index 100% rename from libs/utils/gtest/src/PropertiesSerializationTestSuite.cc rename to libs/utils/gtest/src/PropertiesEncodingTestSuite.cc diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_encoding.c similarity index 100% rename from libs/utils/src/properties_serialization.c rename to libs/utils/src/properties_encoding.c From 72c785dfc706630c87322604b4363542ed9c0f5f Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Tue, 9 Apr 2024 23:16:02 +0200 Subject: [PATCH 06/25] gh-685: Add initial setup for properties decode flags --- .../gtest/src/PropertiesEncodingTestSuite.cc | 197 +++++++++++++----- libs/utils/gtest/src/PropertiesTestSuite.cc | 7 + libs/utils/include/celix_properties.h | 14 +- libs/utils/src/properties_encoding.c | 81 ++++--- 4 files changed, 217 insertions(+), 82 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 4aa2469d5..d346f5f65 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -252,6 +252,49 @@ TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysWithCollisionTest) { json_decref(root); } + +//TODO check desired behaviour, currently every "/" leads to a new object (except if an collision occurs) +//TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSpecialKeyNamesTest) { +// //Given a properties set with special key names (slashes) +// celix_autoptr(celix_properties_t) props = celix_properties_create(); +// celix_properties_set(props, "/", "value1"); +// celix_properties_set(props, "keyThatEndsWithSlash/", "value2"); +// celix_properties_set(props, "key//With//Double//Slash", "value3"); +// celix_properties_set(props, "object/", "value5"); +// celix_properties_set(props, "object//", "value4"); +// celix_properties_set(props, "object/keyThatEndsWithSlash/", "value6"); +// celix_properties_set(props, "object/key//With//Double//Slash", "value7"); +// +// //And an in-memory stream +// celix_autofree char* buf = nullptr; +// size_t bufLen = 0; +// FILE* stream = open_memstream(&buf, &bufLen); +// +// //When encoding the properties to the stream +// auto status = celix_properties_encodeToStream(props, stream, 0); +// ASSERT_EQ(CELIX_SUCCESS, status); +// +// std::cout << buf << std::endl; +// +// //Then the stream contains the JSON representation snippets of the properties +// fclose(stream); +// EXPECT_NE(nullptr, strstr(buf, R"("/":"value1")")) << "JSON: " << buf; +// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value2")")) << "JSON: " << buf; +// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value3")")) << "JSON: " << buf; +// EXPECT_NE(nullptr, strstr(buf, R"("object/":"value5")")) << "JSON: " << buf; +// EXPECT_NE(nullptr, strstr(buf, R"("/":"value5")")) << "JSON: " << buf; //child of object +// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value6")")) << "JSON: " << buf; //child of object +// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value7")")) << "JSON: " << buf; //child of object +// +// +// //And the buf is a valid JSON object +// json_error_t error; +// json_t* root = json_loads(buf, 0, &error); +// EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; +// json_decref(root); +//} + + TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) { //Given an empty JSON object const char* json = "{}"; @@ -392,8 +435,6 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { R"({)", // invalid JSON (caught by jansson) R"([])", // unsupported JSON (top level array not supported) R"(42)", // invalid JSON (caught by jansson) - R"({"mixedArr":["string", true]})", // Mixed array, not supported - R"({"key1":null})", // Null value, not supported }; for (auto& invalidInput: invalidInputs) { //Given an invalid JSON object @@ -477,7 +518,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa "key1":true } }, - "object1/object2/key1":6 + "object1/object2/key1":6, + "key2":2, + "key2":3 })"; // And a stream with the JSON object @@ -487,7 +530,22 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa celix_autoptr(celix_properties_t) props = nullptr; auto status = celix_properties_decodeFromStream(stream, 0, &props); - // Then loading fails + // Then loading succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the properties object contains the last values of the jpath keys + EXPECT_EQ(2, celix_properties_size(props)); + EXPECT_EQ(6, celix_properties_getLong(props, "object1/object2/key1", 0)); + EXPECT_EQ(3, celix_properties_getLong(props, "key2", 0)); + + // When the stream is reset + fseek(stream, 0, SEEK_SET); + + // And decoding the properties from the stream using a flog that does not allow collisions + celix_autoptr(celix_properties_t) props2 = nullptr; + status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); + + // Then loading fails, because of a duplicate key EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); // And at least one error message is added to celix_err @@ -495,51 +553,88 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa celix_err_printErrors(stderr, "Error: ", "\n"); } -//TODO -//TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabledTest) { -// auto invalidInputs = { -// R"({"mixedArr":["string", true]})", // Mixed array gives error on strict -// R"({"key1":null})", // Null value gives error on strict -// R"({"":"value"})", // "" key gives error on strict -// R"({"emptyArr":[]})", // Empty array gives error on strict -// R"({"key1":"val1", "key1:"val2"})", // Duplicate key gives error on strict -// }; -// -// for (auto& invalidInput: invalidInputs) { -// //Given an invalid JSON object -// FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); -// -// //When decoding the properties from the stream with an empty flags -// celix_autoptr(celix_properties_t) props = nullptr; -// auto status = celix_properties_decodeFromStream(stream, 0, &props); -// -// //Then decoding succeeds, because strict is disabled -// ASSERT_EQ(CELIX_SUCCESS, status); -// EXPECT_GE(celix_err_getErrorCount(), 0); -// -// //But the properties object is empty, because the invalid input is ignored -// EXPECT_EQ(0, celix_properties_size(props)); -// -// fclose(stream); -// } -// -// for (auto& invalidInput: invalidInputs) { -// //Given an invalid JSON object -// FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); -// -// //When decoding the properties from the stream with a strict flag -// celix_autoptr(celix_properties_t) props = nullptr; -// auto status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props); -// -// //Then decoding fails -// EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); -// -// //And at least one error message is added to celix_err -// EXPECT_GE(celix_err_getErrorCount(), 1); -// celix_err_printErrors(stderr, "Error: ", "\n"); -// -// fclose(stream); -// } -//} +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabledTest) { + auto invalidInputs = { + R"({"mixedArr":["string", true]})", // Mixed array gives error on strict + R"({"key1":null})", // Null value gives error on strict + R"({"":"value"})", // "" key gives error on strict + R"({"emptyArr":[]})", // Empty array gives error on strict + R"({"key1":"val1", "key1":"val2"})",// Duplicate key gives error on strict + R"({"nullArr":[null,null]})", // Array with null values gives error on strict + }; + + for (auto& invalidInput: invalidInputs) { + //Given an invalid JSON object + FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); + + //When decoding the properties from the stream with an empty flags + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_decodeFromStream(stream, 0, &props); + celix_err_printErrors(stderr, "Error: ", "\n"); + + //Then decoding succeeds, because strict is disabled + ASSERT_EQ(CELIX_SUCCESS, status); + EXPECT_GE(celix_err_getErrorCount(), 0); + + //But the properties size is 0 or 1, because the all invalid inputs are ignored, except the duplicate key + auto size = celix_properties_size(props); + EXPECT_TRUE(size == 0 || size == 1); + + fclose(stream); + } + + for (auto& invalidInput: invalidInputs) { + //Given an invalid JSON object + FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); + + //When decoding the properties from the stream with a strict flag + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props); + + //Then decoding fails + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); + + fclose(stream); + } +} + +TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest) { + // Given a complex JSON object + const char* jsonInput = R"({ + "/": "value1", + "keyThatEndsWithSlash/": "value2", + "key//With//Double//Slash": "value3", + "object": { + "/": "value4", + "keyThatEndsWithSlash/": "value5", + "key//With//Double//Slash": "value6" + } + })"; + + // And a stream with the JSON object + FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); + + // When decoding the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_decodeFromStream(stream, 0, &props); + celix_err_printErrors(stderr, "Error: ", "\n"); + ASSERT_EQ(CELIX_SUCCESS, status); + + // Then the properties object contains the nested objects + EXPECT_EQ(6, celix_properties_size(props)); + EXPECT_STREQ("value1", celix_properties_getString(props, "/")); + EXPECT_STREQ("value2", celix_properties_getString(props, "keyThatEndsWithSlash/")); + EXPECT_STREQ("value3", celix_properties_getString(props, "key//With//Double//Slash")); + EXPECT_STREQ("value4", celix_properties_getString(props, "object//")); + EXPECT_STREQ("value5", celix_properties_getString(props, "object/keyThatEndsWithSlash/")); + EXPECT_STREQ("value6", celix_properties_getString(props, "object/key//With//Double//Slash")); +} -//TODO test with key starting and ending with slash +//TODO test with invalid version string +//TODO is there a strict option needed for version (e.g. not parseable as version handle as string) +//TODO test encoding flags +//TODO error injection tests and wrappers for jansson functions \ No newline at end of file diff --git a/libs/utils/gtest/src/PropertiesTestSuite.cc b/libs/utils/gtest/src/PropertiesTestSuite.cc index 9c1825b51..e1b9391bc 100644 --- a/libs/utils/gtest/src/PropertiesTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesTestSuite.cc @@ -959,3 +959,10 @@ TEST_F(PropertiesTestSuite, SetArrayListWithIllegalArgumentsTest) { //And when an NULL key is used, a ILLEGAL_ARGUMENT error is returned EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_setArrayList(props, nullptr, list)); } + +TEST_F(PropertiesTestSuite, EmptyStringKeyTest) { + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "", "value"); // "" is a valid key (nullptr is not) + EXPECT_EQ(1, celix_properties_size(props)); + EXPECT_STREQ("value", celix_properties_getString(props, "")); +} diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index e501071c0..b82ad9ad4 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -180,9 +180,16 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro const char* file, const char* header); +//TODO document the encode flags #define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 #define CELIX_PROPERTIES_ENCODE_SORT_KEYS 0x02 +//TODO doc. Not encode does not reset the stream position. +CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, + FILE* stream, + int encodeFlags); + +//TODO document the decode flags #define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 #define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x02 #define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x04 @@ -193,12 +200,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | \ CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) -//TODO doc -CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, - FILE* stream, - int encodeFlags); - -//TODO doc +//TODO doc. Note decode does not reset the stream position. CELIX_UTILS_EXPORT celix_status_t celix_properties_decodeFromStream(FILE* stream, int decodeFlags, celix_properties_t** out); diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index a75c9f57c..ba30b670f 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -28,7 +28,8 @@ #include #include -static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, json_t* jsonValue); +static celix_status_t +celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags); // TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection @@ -225,14 +226,19 @@ celix_status_t celix_properties_encodeToStream(const celix_properties_t* propert } } - int rc = - json_dumpf(root, - stream, - JSON_COMPACT); // TODO make celix properties flags for COMPACT and INDENT and maybe other json flags + size_t jsonFlags = JSON_COMPACT; + if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) { + jsonFlags = JSON_INDENT(2); + } + if (encodeFlags & CELIX_PROPERTIES_ENCODE_SORT_KEYS) { + jsonFlags |= JSON_SORT_KEYS; + } + + int rc = json_dumpf(root, stream, jsonFlags); json_decref(root); if (rc != 0) { celix_err_push("Failed to dump json object"); - return CELIX_ENOMEM; // TODO improve error + return CELIX_ENOMEM; } return CELIX_SUCCESS; } @@ -296,11 +302,9 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra // mixed real and integer, ok continue; } else if (type != json_typeof(value)) { - celix_err_push("Mixed types in array"); return CELIX_ILLEGAL_ARGUMENT; } else if (versionType) { if (json_typeof(value) != JSON_STRING || !celix_properties_isVersionString(json_string_value(value))) { - celix_err_push("Mixed version and non-version strings in array"); return CELIX_ILLEGAL_ARGUMENT; } } @@ -320,19 +324,25 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra case JSON_FALSE: *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL; break; + case JSON_NULL: default: - celix_err_pushf("Unexpected json array type %d", type); - return CELIX_ILLEGAL_ARGUMENT; + return CELIX_ILLEGAL_ARGUMENT; // TODO Add test for this case and maybe return a different error code and log error } return CELIX_SUCCESS; } -static celix_status_t celix_properties_loadArray(celix_properties_t* props, const char* key, const json_t* jsonArray) { +static celix_status_t +celix_properties_decodeArray(celix_properties_t* props, const char* key, const json_t* jsonArray, int flags) { celix_array_list_element_type_t elType; celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType); - if (status != CELIX_SUCCESS) { + if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)) { + celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY); + celix_err_pushf("Unsupported mixed or null array for key '%s': %s", key, arrStr); return status; + } else if (status != CELIX_SUCCESS) { + //ignore mixed types + return CELIX_SUCCESS; } celix_array_list_create_options_t opts = CELIX_EMPTY_ARRAY_LIST_CREATE_OPTIONS; @@ -368,7 +378,7 @@ static celix_status_t celix_properties_loadArray(celix_properties_t* props, cons } default: // LCOV_EXCL_START - celix_err_pushf("Unexpected array list element type %d", elType); + celix_err_pushf("Unexpected array list element type %d for key %s", elType, key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -379,8 +389,17 @@ static celix_status_t celix_properties_loadArray(celix_properties_t* props, cons return celix_properties_assignArrayList(props, key, celix_steal_ptr(array)); } -static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, json_t* jsonValue) { - if (celix_properties_hasKey(props, key)) { +static celix_status_t +celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags) { + if (strncmp(key, "", 1) == 0) { + if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS) { + celix_err_push("Key cannot be empty"); + return CELIX_ILLEGAL_ARGUMENT; + } + return CELIX_SUCCESS; // ignore empty keys. + } + + if (celix_properties_hasKey(props, key) && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES)) { celix_err_pushf("Key `%s` already exists.", key); return CELIX_ILLEGAL_ARGUMENT; } @@ -410,20 +429,28 @@ static celix_status_t celix_properties_loadValue(celix_properties_t* props, cons celix_err_push("Failed to create sub key"); return CELIX_ENOMEM; } - status = celix_properties_loadValue(props, subKey, fieldValue); + status = celix_properties_decodeValue(props, subKey, fieldValue, flags); if (status != CELIX_SUCCESS) { return status; } } return CELIX_SUCCESS; } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) { - // empty array -> treat as unset property. silently ignore + if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) { + celix_err_pushf("Unexpected empty array for key `%s`", key); + return CELIX_ILLEGAL_ARGUMENT; + } + // ignore empty arrays return CELIX_SUCCESS; } else if (json_is_array(jsonValue)) { - status = celix_properties_loadArray(props, key, jsonValue); + status = celix_properties_decodeArray(props, key, jsonValue, flags); } else if (json_is_null(jsonValue)) { - celix_err_pushf("Unexpected null value for key `%s`", key); - return CELIX_ILLEGAL_ARGUMENT; + if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) { + celix_err_pushf("Unexpected null value for key `%s`", key); + return CELIX_ILLEGAL_ARGUMENT; + } + // ignore null values + return CELIX_SUCCESS; } else { // LCOV_EXCL_START celix_err_pushf("Unexpected json value type for key `%s`", key); @@ -433,7 +460,7 @@ static celix_status_t celix_properties_loadValue(celix_properties_t* props, cons return status; } -static celix_status_t celix_properties_decodeFromJson(json_t* obj, celix_properties_t** out) { +static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, celix_properties_t** out) { if (!json_is_object(obj)) { celix_err_push("Expected json object"); return CELIX_ILLEGAL_ARGUMENT; @@ -447,7 +474,7 @@ static celix_status_t celix_properties_decodeFromJson(json_t* obj, celix_propert const char* key; json_t* value; json_object_foreach(obj, key, value) { - celix_status_t status = celix_properties_loadValue(props, key, value); + celix_status_t status = celix_properties_decodeValue(props, key, value, flags); if (status != CELIX_SUCCESS) { return status; } @@ -459,11 +486,15 @@ static celix_status_t celix_properties_decodeFromJson(json_t* obj, celix_propert celix_status_t celix_properties_decodeFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) { json_error_t jsonError; - json_t* root = json_loadf(stream, 0, &jsonError); + size_t jsonFlags = 0; + if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) { + jsonFlags = JSON_REJECT_DUPLICATES; + } + json_t* root = json_loadf(stream, jsonFlags, &jsonError); if (!root) { celix_err_pushf("Failed to parse json: %s", jsonError.text); return CELIX_ILLEGAL_ARGUMENT; } - return celix_properties_decodeFromJson(root, out); -} \ No newline at end of file + return celix_properties_decodeFromJson(root, decodeFlags, out); +} From 8e03a828be1f707df23b3fe77b62d491a3adf527 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Wed, 10 Apr 2024 23:27:59 +0200 Subject: [PATCH 07/25] gh-685: Add support for a flat and nested flag for prop encoding --- libs/utils/gtest/src/CelixUtilsTestSuite.cc | 32 ++ .../gtest/src/PropertiesEncodingTestSuite.cc | 435 ++++++++++++------ libs/utils/include/celix_properties.h | 81 +++- libs/utils/include/celix_utils.h | 56 +++ libs/utils/src/properties_encoding.c | 208 ++++++--- 5 files changed, 588 insertions(+), 224 deletions(-) diff --git a/libs/utils/gtest/src/CelixUtilsTestSuite.cc b/libs/utils/gtest/src/CelixUtilsTestSuite.cc index 26bef64b8..95e143d41 100644 --- a/libs/utils/gtest/src/CelixUtilsTestSuite.cc +++ b/libs/utils/gtest/src/CelixUtilsTestSuite.cc @@ -315,6 +315,38 @@ TEST_F(UtilsTestSuite, WriteOrCreateStringTest) { celix_utils_freeStringIfNotEqual(buffer2, out2); } +TEST_F(UtilsTestSuite, WriteOrCreateStringGuardTest) { + // Given a small buffer + char buffer[16]; + + { + // When writing a string that fits in the buffer + char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "abc"); + + // Then the str is equal to the buffer (in this case no malloc was needed) + EXPECT_EQ(buffer, str); + + // And using celix_auto with a string guard + celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buffer, str); + + // Then the guard will not free the string when going out of scope + } + + { + // When writing a string that does not fit in the buffer + char* str = celix_utils_writeOrCreateString( + buffer, sizeof(buffer), "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"); + + // Then the str is not equal to the buffer (in this case a malloc was needed) + EXPECT_NE(buffer, str); + + // And using celix_auto with a string guard + celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buffer, str); + + // Then the guard will free the string when going out of scope + } +} + TEST_F(UtilsTestSuite, StrDupAndStrLenTest) { celix_autofree char* str = celix_utils_strdup("abc"); ASSERT_NE(nullptr, str); diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index d346f5f65..84ed5d0a4 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -32,7 +32,7 @@ class PropertiesSerializationTestSuite : public ::testing::Test { PropertiesSerializationTestSuite() { celix_err_resetErrors(); } }; -TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) { +TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { //Given an empty properties object celix_autoptr(celix_properties_t) props = celix_properties_create(); @@ -41,8 +41,8 @@ TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream, 0); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains an empty JSON object @@ -50,7 +50,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) { EXPECT_STREQ("{}", buf); } -TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) { +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { //Given a properties object with single values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); @@ -65,8 +65,8 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream, 0); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties @@ -85,7 +85,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) { json_decref(root); } -TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithNaNAndInfValuesTest) { +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) { //Given a NAN, INF and -INF value auto keys = {"NAN", "INF", "-INF"}; for (const auto& key : keys) { @@ -102,7 +102,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithNaNAndInfValuesTest //Then saving the properties to the stream fails, because JSON does not support NAN, INF and -INF celix_err_resetErrors(); - auto status = celix_properties_encodeToStream(props, stream, 0); + auto status = celix_properties_saveToStream(props, stream, 0); EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); //And an error msg is added to celix_err @@ -111,7 +111,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithNaNAndInfValuesTest } -TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) { +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { // Given a properties object with array list values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_array_list_t* list1 = celix_arrayList_createStringArray(); @@ -141,7 +141,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) { FILE* stream = open_memstream(&buf, &bufLen); // When saving the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + auto status = celix_properties_saveToStream(props, stream, 0); ASSERT_EQ(CELIX_SUCCESS, status); // Then the stream contains the JSON representation snippets of the properties @@ -161,7 +161,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) { } -TEST_F(PropertiesSerializationTestSuite, EncodeEmptyArrayTest) { +TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { //Given a properties object with an empty array list of with el types string, long, double, bool, version celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_assignArrayList(props, "key1", celix_arrayList_createStringArray()); @@ -171,21 +171,28 @@ TEST_F(PropertiesSerializationTestSuite, EncodeEmptyArrayTest) { celix_properties_assignArrayList(props, "key5", celix_arrayList_createVersionArray()); EXPECT_EQ(5, celix_properties_size(props)); - //And an in-memory stream - celix_autofree char* buf = nullptr; - size_t bufLen = 0; - FILE* stream = open_memstream(&buf, &bufLen); + //When saving the properties to a string + char* output = nullptr; + auto status = celix_properties_saveToString(props, 0, &output); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //Then the save went ok ASSERT_EQ(CELIX_SUCCESS, status); - //Then the stream contains an empty JSON object, because empty arrays are treated as unset - fclose(stream); - EXPECT_STREQ("{}", buf); + //And the output contains an empty JSON object, because empty arrays are treated as unset + EXPECT_STREQ("{}", output); + + //When saving the properties to a string with an error on empty array flag + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output); + + //Then the save fails, because the empty array generates an error + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); } -TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) { +TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { //Given a properties object with jpath keys celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); @@ -200,8 +207,8 @@ TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream, CELIX_PROPERTIES_ENCODE_NESTED); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties @@ -219,90 +226,185 @@ TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) { json_decref(root); } -TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysWithCollisionTest) { +TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { + // note this tests depends on the key iteration order for properties and + // properties key order is based on hash order of the keys, so this test can change if the string hash map + // implementation changes. + //Given a properties object with jpath keys that collide celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1/key2/key3", "value1"); - celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2" - celix_properties_set(props, "key4/key5/key6", "value3"); - celix_properties_set(props, "key4/key5/key6/key7", "value4"); //collision with field "key3/key4/key5" + celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2/key3" -> overwrite + celix_properties_set(props, "key4/key5/key6/key7", "value4"); + celix_properties_set(props, "key4/key5/key6", "value3"); //collision with field "key4/key5/key6/key7" -> overwrite - //And an in-memory stream - celix_autofree char* buf = nullptr; - size_t bufLen = 0; - FILE* stream = open_memstream(&buf, &bufLen); - - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to a string + celix_autofree char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); ASSERT_EQ(CELIX_SUCCESS, status); - //Then the stream contains the JSON representation snippets of the properties - fclose(stream); - EXPECT_NE(nullptr, strstr(buf, R"("key1":{"key2":{"key3":"value1"}})")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key1/key2":"value2")")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key4/key5/key6":"value3")")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key4":{"key5":{"key6":{"key7":"value4"}}})")) << "JSON: " << buf; - //Note whether "key1/key2/key3" or "key1/key2" is serializer first depends on the hash order of the keys, - //so this test can change if the string hash map implementation changes. + //Then the stream contains the JSON representation of the properties with the collisions resolved + EXPECT_NE(nullptr, strstr(output, R"({"key1":{"key2":"value2"},"key4":{"key5":{"key6":"value3"}}})")) + << "JSON: " << output; //And the buf is a valid JSON object json_error_t error; - json_t* root = json_loads(buf, 0, &error); + json_t* root = json_loads(output, 0, &error); EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; json_decref(root); } -//TODO check desired behaviour, currently every "/" leads to a new object (except if an collision occurs) -//TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSpecialKeyNamesTest) { -// //Given a properties set with special key names (slashes) -// celix_autoptr(celix_properties_t) props = celix_properties_create(); -// celix_properties_set(props, "/", "value1"); -// celix_properties_set(props, "keyThatEndsWithSlash/", "value2"); -// celix_properties_set(props, "key//With//Double//Slash", "value3"); -// celix_properties_set(props, "object/", "value5"); -// celix_properties_set(props, "object//", "value4"); -// celix_properties_set(props, "object/keyThatEndsWithSlash/", "value6"); -// celix_properties_set(props, "object/key//With//Double//Slash", "value7"); -// -// //And an in-memory stream -// celix_autofree char* buf = nullptr; -// size_t bufLen = 0; -// FILE* stream = open_memstream(&buf, &bufLen); -// -// //When encoding the properties to the stream -// auto status = celix_properties_encodeToStream(props, stream, 0); -// ASSERT_EQ(CELIX_SUCCESS, status); -// -// std::cout << buf << std::endl; -// -// //Then the stream contains the JSON representation snippets of the properties -// fclose(stream); -// EXPECT_NE(nullptr, strstr(buf, R"("/":"value1")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value2")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value3")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("object/":"value5")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("/":"value5")")) << "JSON: " << buf; //child of object -// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value6")")) << "JSON: " << buf; //child of object -// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value7")")) << "JSON: " << buf; //child of object -// -// -// //And the buf is a valid JSON object -// json_error_t error; -// json_t* root = json_loads(buf, 0, &error); -// EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; -// json_decref(root); -//} - - -TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) { + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTest) { + //Given a properties set with key names with slashes + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "a/key/name/with/slashes", "value1"); + //TODO test separately celix_properties_set(props, "/", "value2"); + celix_properties_set(props, "/keyThatStartsWithSlash", "value3"); + //TODO test separately celix_properties_set(props, "//keyThatStartsWithDoubleSlashes", "value4"); + celix_properties_set(props, "keyThatEndsWithSlash/", "value5"); + celix_properties_set(props, "keyThatEndsWithDoubleSlashes//", "value6"); + celix_properties_set(props, "key//With//Double//Slashes", "value7"); + celix_properties_set(props, "object/keyThatEndsWithSlash/", "value8"); + celix_properties_set(props, "object/keyThatEndsWithDoubleSlashes//", "value9"); + celix_properties_set(props, "object/key//With//Double//Slashes", "value10"); + + + //When saving the properties to a string + char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); + ASSERT_EQ(CELIX_SUCCESS, status); + + //Then the out contains the JSON representation snippets of the properties + EXPECT_NE(nullptr, strstr(output, R"("a":{"key":{"name":{"with":{"slashes":"value1"}}}})")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("keyThatStartsWithSlash":"value3")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value5")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value6")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value7")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value8")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value9")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value10")")) << "JSON: " << output; + + //And the output is a valid JSON object + json_error_t error; + json_t* root = json_loads(output, 0, &error); + EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; + + + //And the structure for (e.g.) value10 is correct + json_t* node = json_object_get(root, "object"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "key"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, ""); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "With"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, ""); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "Double"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, ""); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "Slashes"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_string(node)); + EXPECT_STREQ("value10", json_string_value(node)); + + json_decref(root); +} + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) { + // note this tests depends on the key iteration order for properties and + // properties key order is based on hash order of the keys, so this test can change if the string hash map + // implementation changes. + + //Given a properties that contains keys that will collide with an existing JSON object + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1/key2/key3", "value1"); + celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2" -> overwrite + + //When saving the properties to a string + char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); + + //Then the save succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And both keys are serialized (one as a flat key) (flat key name is whitebox knowledge) + EXPECT_NE(nullptr, strstr(output, R"({"key1":{"key2":"value2"}})")) << "JSON: " << output; + + //When saving the properties to a string with the error on key collision flag + status = celix_properties_saveToString( + props, CELIX_PROPERTIES_ENCODE_NESTED | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output); + + //Then the save fails, because the keys collide + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithAndWithoutStrictFlagTest) { + //Given a properties set with an empty array list + celix_autoptr(celix_properties_t) props = celix_properties_create(); + auto* list = celix_arrayList_createStringArray(); + celix_properties_assignArrayList(props, "key1", list); + + //When saving the properties to a string without the strict flag + char* output = nullptr; + auto status = celix_properties_saveToString(props, 0, &output); + + //Then the save succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + //When saving the properties to a string with the strict flag + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_STRICT, &output); + + //Then the save fails, because the empty array generates an error + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithPrettyPrintTest) { + //Given a properties set with 2 keys + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1", "value1"); + celix_properties_set(props, "key2", "value2"); + + //When saving the properties to a string with pretty print + char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_PRETTY, &output); + + //Then the save succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the output contains the JSON representation snippets of the properties with pretty print (2 indent spaces and + // newlines) + auto* expected = "{\n \"key2\": \"value2\",\n \"key1\": \"value1\"\n}"; + EXPECT_STREQ(expected, output); +} + +TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { //Given an empty JSON object const char* json = "{}"; FILE* stream = fmemopen((void*)json, strlen(json), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object is empty @@ -311,7 +413,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) { fclose(stream); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { //Given a JSON object with single values for types string, long, double, bool and version const char* jsonInput = R"({ "strKey":"strValue", @@ -324,9 +426,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) { //And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object contains the single values @@ -341,7 +443,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) { EXPECT_STREQ("1.2.3.qualifier", vStr); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) { //Given a JSON object with array values for types string, long, double, bool and version const char* jsonInput = R"({ "strArr":["value1","value2"], @@ -356,9 +458,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) { //And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object contains the array values @@ -430,7 +532,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) { EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 3)); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) { auto invalidInputs = { R"({)", // invalid JSON (caught by jansson) R"([])", // unsupported JSON (top level array not supported) @@ -440,9 +542,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { //Given an invalid JSON object FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); //Then loading fails EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -455,25 +557,31 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { } } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithEmptyArrayTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithEmptyArrayTest) { //Given a JSON object with an empty array - auto* emptyArrays = R"({"key1":[]})"; + auto* inputJSON = R"({"key1":[]})"; - //And a stream with the JSON object - FILE* stream = fmemopen((void*)emptyArrays, strlen(emptyArrays), "r"); - - //When decoding the properties from the stream + //When loading the properties from string celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromString2(inputJSON, 0, &props); //Then loading succeeds ASSERT_EQ(CELIX_SUCCESS, status); //And the properties object is empty, because empty arrays are treated as unset EXPECT_EQ(0, celix_properties_size(props)); + + //When loading the properties from string with a strict flag + status = celix_properties_loadFromString2(inputJSON, CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, &props); + + //Then loading fails, because the empty array generates an error + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + ASSERT_GE(celix_err_getErrorCount(), 1); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithNestedObjectsTest) { // Given a complex JSON object const char* jsonInput = R"({ "key1":"value1", @@ -495,9 +603,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsTest) // And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - // When decoding the properties from the stream + // When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); // Then the properties object contains the nested objects @@ -510,40 +618,79 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsTest) EXPECT_EQ(6, celix_properties_getLong(props, "object3/object4/key6", 0)); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPathCollisionTest) { - // Given a complex JSON object with jpath keys that collide +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) { + // Given a complex JSON object with duplicate keys + const char* jsonInput = R"({ + "key":2, + "key":3 + })"; + + // When loading the properties from a string. + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromString2(jsonInput, 0, &props); + + // Then loading succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the properties object contains the last values of the jpath keys + EXPECT_EQ(1, celix_properties_size(props)); + EXPECT_EQ(3, celix_properties_getLong(props, "key", 0)); + + // When decoding the properties from the stream using a flog that does not allow duplicates + celix_autoptr(celix_properties_t) props2 = nullptr; + status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); + + // Then loading fails, because of a duplicate key + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { + // Given a complex JSON object with collisions and duplicate keys + // Collisions: + // - object object1/object2 and value object1/object2 + // - value key1 in object2 in object1 and value object2/key in object1 + // - value object3/key4 and value key4 in object object3 + // Duplicate JSON keys: + // - key3 const char* jsonInput = R"({ "object1": { "object2": { - "key1":true - } + "key1": "value1" + }, + "object2/key2": "value2" }, - "object1/object2/key1":6, - "key2":2, - "key2":3 + "object1/object2" : "value3", + "key3": "value4", + "key3": "value5", + "object3/key4": "value6", + "object3": { + "key4": "value7" + } })"; - // And a stream with the JSON object - FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - - // When decoding the properties from the stream + // When loading the properties from a string. celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromString2(jsonInput, 0, &props); // Then loading succeeds ASSERT_EQ(CELIX_SUCCESS, status); - // And the properties object contains the last values of the jpath keys - EXPECT_EQ(2, celix_properties_size(props)); - EXPECT_EQ(6, celix_properties_getLong(props, "object1/object2/key1", 0)); - EXPECT_EQ(3, celix_properties_getLong(props, "key2", 0)); - - // When the stream is reset - fseek(stream, 0, SEEK_SET); + // And the properties object all the values for the colliding keys and a single (latest) value for the duplicate + // keys + EXPECT_EQ(5, celix_properties_size(props)); + EXPECT_STREQ("value1", celix_properties_getString(props, "object1/object2/key1")); + EXPECT_STREQ("value2", celix_properties_getString(props, "object1/object2/key2")); + EXPECT_STREQ("value3", celix_properties_getString(props, "object1/object2")); + EXPECT_STREQ("value5", celix_properties_getString(props, "key3")); + EXPECT_STREQ("value7", celix_properties_getString(props, "object3/key4")); - // And decoding the properties from the stream using a flog that does not allow collisions + // When decoding the properties from a string using a flag that allows duplicates celix_autoptr(celix_properties_t) props2 = nullptr; - status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); + status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); // Then loading fails, because of a duplicate key EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -551,9 +698,20 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa // And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); celix_err_printErrors(stderr, "Error: ", "\n"); + + // When decoding the properties from a string using a flag that allows collisions + celix_autoptr(celix_properties_t) props3 = nullptr; + status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, &props3); + + // Then loading fails, because of a collision + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabledTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagTest) { auto invalidInputs = { R"({"mixedArr":["string", true]})", // Mixed array gives error on strict R"({"key1":null})", // Null value gives error on strict @@ -567,9 +725,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabl //Given an invalid JSON object FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); - //When decoding the properties from the stream with an empty flags + //When loading the properties from the stream with an empty flags celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); celix_err_printErrors(stderr, "Error: ", "\n"); //Then decoding succeeds, because strict is disabled @@ -587,9 +745,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabl //Given an invalid JSON object FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); - //When decoding the properties from the stream with a strict flag + //When loading the properties from the stream with a strict flag celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props); + auto status = celix_properties_loadFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props); //Then decoding fails EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -602,7 +760,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabl } } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSlashesInTheKeysTest) { // Given a complex JSON object const char* jsonInput = R"({ "/": "value1", @@ -618,9 +776,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest // And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - // When decoding the properties from the stream + // When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); celix_err_printErrors(stderr, "Error: ", "\n"); ASSERT_EQ(CELIX_SUCCESS, status); @@ -636,5 +794,6 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest //TODO test with invalid version string //TODO is there a strict option needed for version (e.g. not parseable as version handle as string) -//TODO test encoding flags -//TODO error injection tests and wrappers for jansson functions \ No newline at end of file +//TODO error injection tests and wrappers for jansson functions +//TODO test load and save with filename +//TODO check all the celix_err messages for consistency diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index b82ad9ad4..04dd04f93 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -180,31 +180,6 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro const char* file, const char* header); -//TODO document the encode flags -#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 -#define CELIX_PROPERTIES_ENCODE_SORT_KEYS 0x02 - -//TODO doc. Not encode does not reset the stream position. -CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, - FILE* stream, - int encodeFlags); - -//TODO document the decode flags -#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x02 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x04 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x04 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x08 -#define CELIX_PROPERTIES_DECODE_STRICT \ - (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | \ - CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | \ - CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) - -//TODO doc. Note decode does not reset the stream position. -CELIX_UTILS_EXPORT celix_status_t celix_properties_decodeFromStream(FILE* stream, - int decodeFlags, - celix_properties_t** out); - /** * @brief Get the entry for a given key in a property set. * @@ -962,6 +937,62 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i !celix_propertiesIterator_isEnd(&(iterName)); \ celix_propertiesIterator_next(&(iterName))) + + +//TODO document the encode flags +#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 +#define CELIX_PROPERTIES_ENCODE_FLAT 0x02 //TODO doc, explain that decoding options ensures all properties entries are written, but only as a top level field entries. +#define CELIX_PROPERTIES_ENCODE_NESTED 0x04 + +#define CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS 0x10 +#define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS 0x20 +#define CELIX_PROPERTIES_ENCODE_STRICT \ + (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) + +//TODO doc +CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const celix_properties_t* properties, + const char* filename, + int encodeFlags); + +//TODO doc. Not encode does not reset or close the stream position. +CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, + FILE* stream, + int encodeFlags); + +//TODO doc +CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_properties_t* properties, + int encodeFlags, + char** out); + + +//TODO document the decode flags +#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS 0x02 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x04 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x08 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x10 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x20 +#define CELIX_PROPERTIES_DECODE_STRICT \ + (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS | \ + CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | \ + CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) + +//TODO doc. Note load2, because load is currently already used. Will be updated in the future. +CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename, + int decodeFlags, + celix_properties_t** out); + +//TODO doc. Note decode does not reset or close the stream position. +CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, + int decodeFlags, + celix_properties_t** out); + +//TODO doc. Note celix_properties_loadFromString2, because loadFromString is currently already used. Will be updated in the future. +CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromString2(const char* input, + int decodeFlags, + celix_properties_t** out); + + #ifdef __cplusplus } #endif diff --git a/libs/utils/include/celix_utils.h b/libs/utils/include/celix_utils.h index 7933b4ebe..6f58f2b76 100644 --- a/libs/utils/include/celix_utils.h +++ b/libs/utils/include/celix_utils.h @@ -29,6 +29,8 @@ extern "C" { #include #include "celix_utils_export.h" +#include "celix_compiler.h" +#include "celix_cleanup.h" #define CELIX_US_IN_SEC (1000000) #define CELIX_NS_IN_SEC ((CELIX_US_IN_SEC)*1000) @@ -91,6 +93,60 @@ __attribute__((format(printf, 3, 0))); */ CELIX_UTILS_EXPORT void celix_utils_freeStringIfNotEqual(const char* buffer, char* str); +/** + * @brief Guard for a string created with celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString. + * + * Can be used with celix_auto() to automatically and correctly free the string. + * If the string is pointing to the buffer, the string should be freed, otherwise the string should be freed. + * + * + */ +typedef struct celix_utils_string_guard { + const char* buffer; + char* string; +} celix_utils_string_guard_t; + +/** + * @brief Initialize a guard for a string created with celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString. + * + * De-initialize with celix_utils_stringGuard_deinit(). + * + * No allocation is performed. + * This is intended to be used with celix_auto(). + * + * * Example: +* ``` + * const char* possibleLongString = ... + * char buffer[64]; + * char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "Hello %s", possibleLongString); + * celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buffer, str); + * ``` + * If the strGuard goes out of scope, the string will be freed correctly. + * + * @param buffer A (local) buffer which was potentially used to create the string. + * @param string The string to guard. + * @return An initialized string guard to be used with celix_auto(). + */ +static CELIX_UNUSED inline celix_utils_string_guard_t celix_utils_stringGuard_init(const char* buffer, char* string) { + celix_utils_string_guard_t guard; + guard.buffer = buffer; + guard.string = string; + return guard; +} + +/** + * @brief De-initialize a string guard. + * + * This will free the string if it is not equal to the buffer. + * This is intended to be used with celix_auto(). + * + * @param guard The guard to de-initialize. + */ +static CELIX_UNUSED inline void celix_utils_stringGuard_deinit(celix_utils_string_guard_t* guard) { + celix_utils_freeStringIfNotEqual(guard->buffer, guard->string); +} + +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_utils_string_guard_t, celix_utils_stringGuard_deinit) /** * @brief Compares two strings and returns true if the strings are equal. diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index ba30b670f..e1ad99b0d 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -31,8 +31,6 @@ static celix_status_t celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags); -// TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection - static celix_status_t celix_properties_versionToJson(const celix_version_t* version, json_t** out) { celix_autofree char* versionStr = celix_version_toString(version); if (!versionStr) { @@ -68,7 +66,7 @@ static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_ return celix_properties_versionToJson(entry.versionVal, out); default: // LCOV_EXCL_START - celix_err_pushf("Unexpected array list element type %d", elType); + celix_err_pushf("Invalid array list element type %d", elType); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -79,9 +77,16 @@ static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_ return CELIX_SUCCESS; } -static celix_status_t celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry, json_t** out) { +static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, + const celix_properties_entry_t* entry, + int flags, + json_t** out) { *out = NULL; if (celix_arrayList_size(entry->typed.arrayValue) == 0) { + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) { + celix_err_pushf("Invalid empty array for key %s", key); + return CELIX_ILLEGAL_ARGUMENT; + } return CELIX_SUCCESS; // empty array -> treat as unset property } @@ -115,7 +120,8 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const celix_propert return CELIX_SUCCESS; } -static celix_status_t celix_properties_entryValueToJson(const celix_properties_entry_t* entry, json_t** out) { +static celix_status_t +celix_properties_entryValueToJson(const char* key, const celix_properties_entry_t* entry, int flags, json_t** out) { *out = NULL; switch (entry->valueType) { case CELIX_PROPERTIES_VALUE_TYPE_STRING: @@ -126,7 +132,7 @@ static celix_status_t celix_properties_entryValueToJson(const celix_properties_e break; case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE: if (isnan(entry->typed.doubleValue) || isinf(entry->typed.doubleValue)) { - celix_err_pushf("Double NaN or Inf not supported in JSON."); + celix_err_pushf("Invalid NaN or Inf in key '%s'.", key); return CELIX_ILLEGAL_ARGUMENT; } *out = json_real(entry->typed.doubleValue); @@ -137,34 +143,76 @@ static celix_status_t celix_properties_entryValueToJson(const celix_properties_e case CELIX_PROPERTIES_VALUE_TYPE_VERSION: return celix_properties_versionToJson(entry->typed.versionValue, out); case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST: - return celix_properties_arrayEntryValueToJson(entry, out); + return celix_properties_arrayEntryValueToJson(key, entry, flags, out); default: // LCOV_EXCL_START - celix_err_pushf("Unexpected properties entry type %d", entry->valueType); + celix_err_pushf("Invalid properties entry type %d.", entry->valueType); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } if (!*out) { - celix_err_push("Failed to create json value"); + celix_err_pushf("Failed to create json value for key '%s'.", key); return CELIX_ENOMEM; } return CELIX_SUCCESS; } -static celix_status_t -celix_properties_addEntryToJson(const celix_properties_entry_t* entry, const char* key, json_t* root) { +static celix_status_t celix_properties_addJsonValueToJson(json_t* value, const char* key, json_t* obj, int flags) { + if (!value) { + // ignore unset values + return CELIX_SUCCESS; + } + + json_t* field = json_object_get(obj, key); + if (field) { + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { + celix_err_pushf("Invalid key collision. key '%s' already exists.", key); + return CELIX_ILLEGAL_ARGUMENT; + } + } + + int rc = json_object_set_new(obj, key, value); + if (rc != 0) { + celix_err_push("Failed to set json object"); + return CELIX_ENOMEM; + } + return CELIX_SUCCESS; +} + +static celix_status_t celix_properties_addPropertiesEntryFlatToJson(const celix_properties_entry_t* entry, + const char* key, + json_t* root, + int flags) { + json_t* value; + celix_status_t status = celix_properties_entryValueToJson(key, entry, flags, &value); + if (status != CELIX_SUCCESS) { + return status; + } + return celix_properties_addJsonValueToJson(value, key, root, flags); +} + +static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_properties_entry_t* entry, + const char* key, + json_t* root, + int flags) { json_t* jsonObj = root; - const char* subKey = key; + const char* fieldName = key; const char* slash = strstr(key, "/"); while (slash) { - celix_autofree const char* name = strndup(subKey, slash - subKey); + char buf[64]; + char* name = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)(slash - fieldName), fieldName); + celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, name); if (!name) { celix_err_push("Failed to create name string"); return CELIX_ENOMEM; } json_t* subObj = json_object_get(jsonObj, name); - if (!subObj) { + if (!subObj || !json_is_object(subObj)) { + if (!json_is_object(subObj) && flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { + celix_err_pushf("Invalid key collision. Key '%s' already exists.", name); + return CELIX_ILLEGAL_ARGUMENT; + } subObj = json_object(); if (!subObj) { celix_err_push("Failed to create json object"); @@ -175,51 +223,40 @@ celix_properties_addEntryToJson(const celix_properties_entry_t* entry, const cha celix_err_push("Failed to set json object"); return CELIX_ENOMEM; } - } else if (!json_is_object(subObj)) { - // subObj is not an object, so obj cannot be added -> adding obj flat - jsonObj = root; - subKey = key; - break; } jsonObj = subObj; - subKey = slash + 1; - slash = strstr(subKey, "/"); - - json_t* field = json_object_get(jsonObj, subKey); - if (field) { - // field already exists, so adding obj flat - jsonObj = root; - subKey = key; - break; - } + fieldName = slash + 1; + slash = strstr(fieldName, "/"); } json_t* value; - celix_status_t status = celix_properties_entryValueToJson(entry, &value); + celix_status_t status = celix_properties_entryValueToJson(fieldName, entry, flags, &value); if (status != CELIX_SUCCESS) { return status; - } else if (!value) { - // ignore unset values - } else { - int rc = json_object_set_new(jsonObj, subKey, value); - if (rc != 0) { - celix_err_push("Failed to set json object"); - return CELIX_ENOMEM; - } } - - return CELIX_SUCCESS; + return celix_properties_addJsonValueToJson(value, fieldName, jsonObj, flags); } -celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) { +celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) { json_t* root = json_object(); if (!root) { celix_err_push("Failed to create json object"); } + if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED)) { + //no encoding flags set, default to flat + encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT; + } + CELIX_PROPERTIES_ITERATE(properties, iter) { - celix_status_t status = celix_properties_addEntryToJson(&iter.entry, iter.key, root); + celix_status_t status; + if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) { + status = celix_properties_addPropertiesEntryFlatToJson(&iter.entry, iter.key, root, encodeFlags); + } else { + assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED); + status = celix_properties_addPropertiesEntryToJson(&iter.entry, iter.key, root, encodeFlags); + } if (status != CELIX_SUCCESS) { json_decref(root); return status; @@ -230,9 +267,6 @@ celix_status_t celix_properties_encodeToStream(const celix_properties_t* propert if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) { jsonFlags = JSON_INDENT(2); } - if (encodeFlags & CELIX_PROPERTIES_ENCODE_SORT_KEYS) { - jsonFlags |= JSON_SORT_KEYS; - } int rc = json_dumpf(root, stream, jsonFlags); json_decref(root); @@ -243,6 +277,35 @@ celix_status_t celix_properties_encodeToStream(const celix_properties_t* propert return CELIX_SUCCESS; } +celix_status_t celix_properties_save(const celix_properties_t* properties, const char* filename, int encodeFlags) { + FILE* stream = fopen(filename, "w"); + if (!stream) { + celix_err_pushf("Failed to open file %s", filename); + return CELIX_FILE_IO_EXCEPTION; + } + celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); + fclose(stream); + return status; +} + +celix_status_t celix_properties_saveToString(const celix_properties_t* properties, int encodeFlags, char** out) { + *out = NULL; + celix_autofree char* buffer = NULL; + size_t size = 0; + FILE* stream = open_memstream(&buffer, &size); + if (!stream) { + celix_err_push("Failed to open memstream"); + return CELIX_FILE_IO_EXCEPTION; + } + + celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); + fclose(stream); + if (status == CELIX_SUCCESS) { + *out = celix_steal_ptr(buffer); + } + return status; +} + static celix_version_t* celix_properties_parseVersion(const char* value) { // precondition: value is a valid version string (14 chars prefix and 1 char suffix) celix_version_t* version = NULL; @@ -326,7 +389,7 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra break; case JSON_NULL: default: - return CELIX_ILLEGAL_ARGUMENT; // TODO Add test for this case and maybe return a different error code and log error + return CELIX_ILLEGAL_ARGUMENT; } return CELIX_SUCCESS; @@ -338,7 +401,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType); if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)) { celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY); - celix_err_pushf("Unsupported mixed or null array for key '%s': %s", key, arrStr); + celix_err_pushf("Invalid mixed or null array for key '%s': %s", key, arrStr); return status; } else if (status != CELIX_SUCCESS) { //ignore mixed types @@ -378,7 +441,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j } default: // LCOV_EXCL_START - celix_err_pushf("Unexpected array list element type %d for key %s", elType, key); + celix_err_pushf("Invalid array list element type %d for key %s", elType, key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -399,8 +462,9 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* return CELIX_SUCCESS; // ignore empty keys. } - if (celix_properties_hasKey(props, key) && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES)) { - celix_err_pushf("Key `%s` already exists.", key); + if (!json_is_object(jsonValue) && celix_properties_hasKey(props, key) && + (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS)) { + celix_err_pushf("Invalid key collision. Key '%s' already exists.", key); return CELIX_ILLEGAL_ARGUMENT; } @@ -423,13 +487,14 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* const char* fieldName; json_t* fieldValue; json_object_foreach(jsonValue, fieldName, fieldValue) { - celix_autofree char* subKey; - int rc = asprintf(&subKey, "%s/%s", key, fieldName); - if (rc < 0) { - celix_err_push("Failed to create sub key"); - return CELIX_ENOMEM; + char buf[64]; + char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s/%s", key, fieldName); + celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, combinedKey); + if (!combinedKey) { + celix_err_push("Failed to create sub key"); + return CELIX_ENOMEM; } - status = celix_properties_decodeValue(props, subKey, fieldValue, flags); + status = celix_properties_decodeValue(props, combinedKey, fieldValue, flags); if (status != CELIX_SUCCESS) { return status; } @@ -437,7 +502,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* return CELIX_SUCCESS; } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) { if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) { - celix_err_pushf("Unexpected empty array for key `%s`", key); + celix_err_pushf("Invalid empty array for key '%s'", key); return CELIX_ILLEGAL_ARGUMENT; } // ignore empty arrays @@ -446,14 +511,14 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* status = celix_properties_decodeArray(props, key, jsonValue, flags); } else if (json_is_null(jsonValue)) { if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) { - celix_err_pushf("Unexpected null value for key `%s`", key); + celix_err_pushf("Invalid null value for key '%s'", key); return CELIX_ILLEGAL_ARGUMENT; } // ignore null values return CELIX_SUCCESS; } else { // LCOV_EXCL_START - celix_err_pushf("Unexpected json value type for key `%s`", key); + celix_err_pushf("Invalid json value type for key '%s'", key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -484,7 +549,7 @@ static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, ce return CELIX_SUCCESS; } -celix_status_t celix_properties_decodeFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) { +celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) { json_error_t jsonError; size_t jsonFlags = 0; if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) { @@ -495,6 +560,27 @@ celix_status_t celix_properties_decodeFromStream(FILE* stream, int decodeFlags, celix_err_pushf("Failed to parse json: %s", jsonError.text); return CELIX_ILLEGAL_ARGUMENT; } - return celix_properties_decodeFromJson(root, decodeFlags, out); } + +celix_status_t celix_properties_load2(const char* filename, int decodeFlags, celix_properties_t** out) { + FILE* stream = fopen(filename, "r"); + if (!stream) { + celix_err_pushf("Failed to open file %s", filename); + return CELIX_FILE_IO_EXCEPTION; + } + celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out); + fclose(stream); + return status; +} + +celix_status_t celix_properties_loadFromString2(const char* input, int decodeFlags, celix_properties_t** out) { + FILE* stream = fmemopen((void*)input, strlen(input), "r"); + if (!stream) { + celix_err_push("Failed to open memstream"); + return CELIX_FILE_IO_EXCEPTION; + } + celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out); + fclose(stream); + return status; +} From 0ceaeb8f0f7eddf0e05b156e6c61f583db07ea4b Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Sat, 13 Apr 2024 19:59:35 +0200 Subject: [PATCH 08/25] gh-685: Add additional prop encoding test based on test cov --- .../gtest/src/PropertiesEncodingTestSuite.cc | 132 ++++++++++++++++-- libs/utils/src/properties_encoding.c | 86 ++++++------ 2 files changed, 161 insertions(+), 57 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 84ed5d0a4..b854e1831 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -76,7 +76,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { EXPECT_NE(nullptr, strstr(buf, R"("key3":3)")) << "JSON: " << buf; EXPECT_NE(nullptr, strstr(buf, R"("key4":4.0)")) << "JSON: " << buf; EXPECT_NE(nullptr, strstr(buf, R"("key5":true)")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key6":"celix_version<1.2.3.qualifier>")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key6":"version<1.2.3.qualifier>")")) << "JSON: " << buf; //And the buf is a valid JSON object json_error_t error; @@ -150,7 +150,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { EXPECT_NE(nullptr, strstr(buf, R"("key2":[1,2])")) << "JSON: " << buf; EXPECT_NE(nullptr, strstr(buf, R"("key3":[1.0,2.0])")) << "JSON: " << buf; EXPECT_NE(nullptr, strstr(buf, R"("key4":[true,false])")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key5":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"])")) + EXPECT_NE(nullptr, strstr(buf, R"("key5":["version<1.2.3.qualifier>","version<4.5.6.qualifier>"])")) << "JSON: " << buf; // And the buf is a valid JSON object @@ -260,9 +260,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTe //Given a properties set with key names with slashes celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "a/key/name/with/slashes", "value1"); - //TODO test separately celix_properties_set(props, "/", "value2"); celix_properties_set(props, "/keyThatStartsWithSlash", "value3"); - //TODO test separately celix_properties_set(props, "//keyThatStartsWithDoubleSlashes", "value4"); celix_properties_set(props, "keyThatEndsWithSlash/", "value5"); celix_properties_set(props, "keyThatEndsWithDoubleSlashes//", "value6"); celix_properties_set(props, "key//With//Double//Slashes", "value7"); @@ -397,6 +395,24 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithPrettyPrintTest) { EXPECT_STREQ(expected, output); } +TEST_F(PropertiesSerializationTestSuite, SaveWithInvalidStreamTest) { + celix_autoptr(celix_properties_t) properties = celix_properties_create(); + celix_properties_set(properties, "key", "value"); + + // Saving properties with invalid stream will fail + auto status = celix_properties_save(properties, "/non-existing/no/rights/file.json", 0); + EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status); + EXPECT_EQ(1, celix_err_getErrorCount()); + + auto* readStream = fopen("/dev/null", "r"); + status = celix_properties_saveToStream(properties, readStream, 0); + EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status); + EXPECT_EQ(2, celix_err_getErrorCount()); + fclose(readStream); + + celix_err_printErrors(stderr, "Error: ", "\n"); +} + TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { //Given an empty JSON object const char* json = "{}"; @@ -420,7 +436,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { "longKey":42, "doubleKey":2.0, "boolKey":true, - "versionKey":"celix_version<1.2.3.qualifier>" + "versionKey":"version<1.2.3.qualifier>" })"; //And a stream with the JSON object @@ -450,7 +466,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) { "intArr":[1,2], "realArr":[1.0,2.0], "boolArr":[true,false], - "versionArr":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"], + "versionArr":["version<1.2.3.qualifier>","version<4.5.6.qualifier>"], "mixedRealAndIntArr1":[1,2.0,2,3.0], "mixedRealAndIntArr2":[1.0,2,2.0,3] })"; @@ -714,6 +730,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagTest) { auto invalidInputs = { R"({"mixedArr":["string", true]})", // Mixed array gives error on strict + R"({"mixedVersionAndStringArr":["version<1.2.3.qualifier>","2.3.4"]})", // Mixed array gives error on strict R"({"key1":null})", // Null value gives error on strict R"({"":"value"})", // "" key gives error on strict R"({"emptyArr":[]})", // Empty array gives error on strict @@ -728,7 +745,6 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagT //When loading the properties from the stream with an empty flags celix_autoptr(celix_properties_t) props = nullptr; auto status = celix_properties_loadFromStream(stream, 0, &props); - celix_err_printErrors(stderr, "Error: ", "\n"); //Then decoding succeeds, because strict is disabled ASSERT_EQ(CELIX_SUCCESS, status); @@ -792,8 +808,100 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSlashesInTheKeysTest) EXPECT_STREQ("value6", celix_properties_getString(props, "object/key//With//Double//Slash")); } -//TODO test with invalid version string -//TODO is there a strict option needed for version (e.g. not parseable as version handle as string) -//TODO error injection tests and wrappers for jansson functions -//TODO test load and save with filename -//TODO check all the celix_err messages for consistency +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidVersionsTest) { + // Given a JSON object with an invalid version string (<, > not allowed in qualifier) + const auto* jsonInput = R"( {"key":"version<1.2.3.>"} )"; + + // When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromString2(jsonInput, 0, &props); + + // Then loading fails + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); + + // Given a JSON object with an invalid version strings, that are not recognized as versions + jsonInput = + R"( {"key1":"version<1.2.3", "key2":"version1.2.3>", "key3":"ver<1.2.3>}", "key4":"celix_version<1.2.3>"} )"; + + // When loading the properties from the stream + status = celix_properties_loadFromString2(jsonInput, 0, &props); + + // Then loading succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // But the values are not recognized as versions + EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key1")); + EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key2")); + EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key3")); + EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key4")); +} + +TEST_F(PropertiesSerializationTestSuite, LoadWithInvalidStreamTest) { + celix_properties_t* dummyProps = nullptr; + + // Loading properties with invalid stream will fail + auto status = celix_properties_load2("non_existing_file.json", 0, &dummyProps); + EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status); + EXPECT_EQ(1, celix_err_getErrorCount()); + + char* buf = nullptr; + size_t size = 0; + FILE* stream = open_memstream(&buf, &size); // empty stream + status = celix_properties_loadFromStream(stream, 0, &dummyProps); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + EXPECT_EQ(2, celix_err_getErrorCount()); + + fclose(stream); + free(buf); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) { + // Given a properties object with all possible types (but no empty arrays) + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "strKey", "strValue"); + celix_properties_setLong(props, "longKey", 42); + celix_properties_setDouble(props, "doubleKey", 2.0); + celix_properties_setBool(props, "boolKey", true); + celix_properties_setVersion(props, "versionKey", celix_version_create(1, 2, 3, "qualifier")); + auto* strArr = celix_arrayList_createStringArray(); + celix_arrayList_addString(strArr, "value1"); + celix_arrayList_addString(strArr, "value2"); + auto* longArr = celix_arrayList_createLongArray(); + celix_arrayList_addLong(longArr, 1); + celix_arrayList_addLong(longArr, 2); + celix_properties_assignArrayList(props, "longArr", longArr); + auto* doubleArr = celix_arrayList_createDoubleArray(); + celix_arrayList_addDouble(doubleArr, 1.0); + celix_arrayList_addDouble(doubleArr, 2.0); + celix_properties_assignArrayList(props, "doubleArr", doubleArr); + auto* boolArr = celix_arrayList_createBoolArray(); + celix_arrayList_addBool(boolArr, true); + celix_arrayList_addBool(boolArr, false); + celix_properties_assignArrayList(props, "boolArr", boolArr); + auto* versionArr = celix_arrayList_createVersionArray(); + celix_arrayList_addVersion(versionArr, celix_version_create(1, 2, 3, "qualifier")); + celix_arrayList_addVersion(versionArr, celix_version_create(4, 5, 6, "qualifier")); + celix_properties_assignArrayList(props, "versionArr", versionArr); + + // When saving the properties to a properties_test.json file + const char* filename = "properties_test.json"; + auto status = celix_properties_save(props, filename, 0); + + // Then saving succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // When loading the properties from the properties_test.json file + celix_autoptr(celix_properties_t) loadedProps = nullptr; + status = celix_properties_load2(filename, 0, &loadedProps); + + // Then loading succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the loaded properties are equal to the original properties + EXPECT_TRUE(celix_properties_equals(props, loadedProps)); +} diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index e1ad99b0d..087590f76 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -34,12 +34,12 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* static celix_status_t celix_properties_versionToJson(const celix_version_t* version, json_t** out) { celix_autofree char* versionStr = celix_version_toString(version); if (!versionStr) { - celix_err_push("Failed to create version string"); + celix_err_push("Failed to create version string."); return CELIX_ENOMEM; } - *out = json_sprintf("celix_version<%s>", versionStr); + *out = json_sprintf("version<%s>", versionStr); if (!*out) { - celix_err_push("Failed to create json string"); + celix_err_push("Failed to create json string."); return CELIX_ENOMEM; } return CELIX_SUCCESS; @@ -66,12 +66,12 @@ static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_ return celix_properties_versionToJson(entry.versionVal, out); default: // LCOV_EXCL_START - celix_err_pushf("Invalid array list element type %d", elType); + celix_err_pushf("Invalid array list element type %d.", elType); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } if (!*out) { - celix_err_push("Failed to create json value"); + celix_err_push("Failed to create json value."); return CELIX_ENOMEM; } return CELIX_SUCCESS; @@ -84,7 +84,7 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, *out = NULL; if (celix_arrayList_size(entry->typed.arrayValue) == 0) { if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) { - celix_err_pushf("Invalid empty array for key %s", key); + celix_err_pushf("Invalid empty array for key %s.", key); return CELIX_ILLEGAL_ARGUMENT; } return CELIX_SUCCESS; // empty array -> treat as unset property @@ -92,7 +92,7 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, json_t* array = json_array(); if (!array) { - celix_err_push("Failed to create json array"); + celix_err_push("Failed to create json array."); return CELIX_ENOMEM; } @@ -109,7 +109,7 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, } else { int rc = json_array_append_new(array, jsonValue); if (rc != 0) { - celix_err_push("Failed to append json string to array"); + celix_err_push("Failed to append json string to array."); json_decref(array); return CELIX_ENOMEM; } @@ -271,8 +271,8 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie int rc = json_dumpf(root, stream, jsonFlags); json_decref(root); if (rc != 0) { - celix_err_push("Failed to dump json object"); - return CELIX_ENOMEM; + celix_err_push("Failed to dump json object to stream."); + return CELIX_FILE_IO_EXCEPTION; } return CELIX_SUCCESS; } @@ -280,7 +280,7 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie celix_status_t celix_properties_save(const celix_properties_t* properties, const char* filename, int encodeFlags) { FILE* stream = fopen(filename, "w"); if (!stream) { - celix_err_pushf("Failed to open file %s", filename); + celix_err_pushf("Failed to open file %s.", filename); return CELIX_FILE_IO_EXCEPTION; } celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); @@ -294,7 +294,7 @@ celix_status_t celix_properties_saveToString(const celix_properties_t* propertie size_t size = 0; FILE* stream = open_memstream(&buffer, &size); if (!stream) { - celix_err_push("Failed to open memstream"); + celix_err_push("Failed to open memstream."); return CELIX_FILE_IO_EXCEPTION; } @@ -306,26 +306,26 @@ celix_status_t celix_properties_saveToString(const celix_properties_t* propertie return status; } -static celix_version_t* celix_properties_parseVersion(const char* value) { - // precondition: value is a valid version string (14 chars prefix and 1 char suffix) - celix_version_t* version = NULL; +static celix_status_t celix_properties_parseVersion(const char* value, celix_version_t** out) { + // precondition: value is a valid version string (8 chars prefix and 1 char suffix) + *out = NULL;; char buf[32]; - char* corrected = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 15, value + 14); + char* corrected = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 9, value + 8); + celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buf, corrected); if (!corrected) { - celix_err_push("Failed to create corrected version string"); - return NULL; + celix_err_push("Failed to create corrected version string."); + return ENOMEM; } - celix_status_t status = celix_version_parse(corrected, &version); - celix_utils_freeStringIfNotEqual(buf, corrected); + celix_status_t status = celix_version_parse(corrected, out); if (status != CELIX_SUCCESS) { - celix_err_push("Failed to parse version string"); - return NULL; + celix_err_push("Failed to parse version string."); + return status; } - return version; + return CELIX_SUCCESS; } static bool celix_properties_isVersionString(const char* value) { - return strncmp(value, "celix_version<", 14) == 0 && value[strlen(value) - 1] == '>'; + return strncmp(value, "version<", 8) == 0 && value[strlen(value) - 1] == '>'; } /** @@ -401,7 +401,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType); if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)) { celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY); - celix_err_pushf("Invalid mixed or null array for key '%s': %s", key, arrStr); + celix_err_pushf("Invalid mixed or null array for key '%s': %s.", key, arrStr); return status; } else if (status != CELIX_SUCCESS) { //ignore mixed types @@ -432,16 +432,14 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j status = celix_arrayList_addBool(array, json_boolean_value(value)); break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: { - celix_version_t* v = celix_properties_parseVersion(json_string_value(value)); - if (!v) { - return CELIX_ILLEGAL_ARGUMENT; - } - status = celix_arrayList_addVersion(array, v); + celix_version_t* v; + status = celix_properties_parseVersion(json_string_value(value), &v); + status = CELIX_DO_IF(status, celix_arrayList_addVersion(array, v)); break; } default: // LCOV_EXCL_START - celix_err_pushf("Invalid array list element type %d for key %s", elType, key); + celix_err_pushf("Invalid array list element type %d for key %s.", elType, key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -456,7 +454,7 @@ static celix_status_t celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags) { if (strncmp(key, "", 1) == 0) { if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS) { - celix_err_push("Key cannot be empty"); + celix_err_push("Key cannot be empty."); return CELIX_ILLEGAL_ARGUMENT; } return CELIX_SUCCESS; // ignore empty keys. @@ -470,11 +468,9 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* celix_status_t status = CELIX_SUCCESS; if (json_is_string(jsonValue) && celix_properties_isVersionString(json_string_value(jsonValue))) { - celix_version_t* version = celix_properties_parseVersion(json_string_value(jsonValue)); - if (!version) { - return CELIX_ILLEGAL_ARGUMENT; - } - status = celix_properties_setVersion(props, key, version); + celix_version_t* version; + status = celix_properties_parseVersion(json_string_value(jsonValue), &version); + status = CELIX_DO_IF(status, celix_properties_setVersion(props, key, version)); } else if (json_is_string(jsonValue)) { status = celix_properties_setString(props, key, json_string_value(jsonValue)); } else if (json_is_integer(jsonValue)) { @@ -491,7 +487,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s/%s", key, fieldName); celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, combinedKey); if (!combinedKey) { - celix_err_push("Failed to create sub key"); + celix_err_push("Failed to create sub key."); return CELIX_ENOMEM; } status = celix_properties_decodeValue(props, combinedKey, fieldValue, flags); @@ -502,7 +498,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* return CELIX_SUCCESS; } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) { if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) { - celix_err_pushf("Invalid empty array for key '%s'", key); + celix_err_pushf("Invalid empty array for key '%s'.", key); return CELIX_ILLEGAL_ARGUMENT; } // ignore empty arrays @@ -511,14 +507,14 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* status = celix_properties_decodeArray(props, key, jsonValue, flags); } else if (json_is_null(jsonValue)) { if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) { - celix_err_pushf("Invalid null value for key '%s'", key); + celix_err_pushf("Invalid null value for key '%s'.", key); return CELIX_ILLEGAL_ARGUMENT; } // ignore null values return CELIX_SUCCESS; } else { // LCOV_EXCL_START - celix_err_pushf("Invalid json value type for key '%s'", key); + celix_err_pushf("Invalid json value type for key '%s'.", key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -527,7 +523,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, celix_properties_t** out) { if (!json_is_object(obj)) { - celix_err_push("Expected json object"); + celix_err_push("Expected json object."); return CELIX_ILLEGAL_ARGUMENT; } @@ -557,7 +553,7 @@ celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, ce } json_t* root = json_loadf(stream, jsonFlags, &jsonError); if (!root) { - celix_err_pushf("Failed to parse json: %s", jsonError.text); + celix_err_pushf("Failed to parse json: %s.", jsonError.text); return CELIX_ILLEGAL_ARGUMENT; } return celix_properties_decodeFromJson(root, decodeFlags, out); @@ -566,7 +562,7 @@ celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, ce celix_status_t celix_properties_load2(const char* filename, int decodeFlags, celix_properties_t** out) { FILE* stream = fopen(filename, "r"); if (!stream) { - celix_err_pushf("Failed to open file %s", filename); + celix_err_pushf("Failed to open file %s.", filename); return CELIX_FILE_IO_EXCEPTION; } celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out); @@ -577,7 +573,7 @@ celix_status_t celix_properties_load2(const char* filename, int decodeFlags, cel celix_status_t celix_properties_loadFromString2(const char* input, int decodeFlags, celix_properties_t** out) { FILE* stream = fmemopen((void*)input, strlen(input), "r"); if (!stream) { - celix_err_push("Failed to open memstream"); + celix_err_push("Failed to open memstream."); return CELIX_FILE_IO_EXCEPTION; } celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out); From 99e503aa62a02173818a7d518c102f7dd8edaa12 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Sun, 14 Apr 2024 16:54:16 +0200 Subject: [PATCH 09/25] gh-685: Add error injection test for properties encoding --- conanfile.py | 4 +- libs/error_injector/jansson/CMakeLists.txt | 2 + .../jansson/include/jansson_ei.h | 2 + libs/error_injector/jansson/src/jansson_ei.cc | 58 ++-- .../celix_version/CMakeLists.txt | 1 + .../celix_version/include/celix_version_ei.h | 2 + .../celix_version/src/celix_version_ei.cc | 7 + libs/utils/gtest/CMakeLists.txt | 2 + ...opertiesEncodingErrorInjectionTestSuite.cc | 305 ++++++++++++++++++ .../gtest/src/PropertiesEncodingTestSuite.cc | 30 +- libs/utils/src/properties.c | 3 +- libs/utils/src/properties_encoding.c | 53 ++- 12 files changed, 403 insertions(+), 66 deletions(-) create mode 100644 libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc diff --git a/conanfile.py b/conanfile.py index 609082c8f..63d9ac005 100644 --- a/conanfile.py +++ b/conanfile.py @@ -309,7 +309,7 @@ def configure(self): self.options['openssl'].shared = True if self.options.build_celix_dfi: self.options['libffi'].shared = True - if self.options.build_celix_dfi or self.options.build_celix_etcdlib: + if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib: self.options['jansson'].shared = True def requirements(self): @@ -332,7 +332,7 @@ def requirements(self): self.requires("civetweb/1.16") if self.options.build_celix_dfi: self.requires("libffi/[>=3.2.1 <4.0.0]") - if self.options.build_celix_dfi or self.options.build_celix_etcdlib: + if self.option.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib: self.requires("jansson/[>=2.12 <3.0.0]") if self.options.build_rsa_discovery_zeroconf: # TODO: To be replaced with mdnsresponder/1790.80.10, resolve some problems of mdnsresponder diff --git a/libs/error_injector/jansson/CMakeLists.txt b/libs/error_injector/jansson/CMakeLists.txt index b536956a0..b093901d6 100644 --- a/libs/error_injector/jansson/CMakeLists.txt +++ b/libs/error_injector/jansson/CMakeLists.txt @@ -35,5 +35,7 @@ target_link_options(jansson_ei INTERFACE LINKER:--wrap,json_integer LINKER:--wrap,json_string LINKER:--wrap,json_real + LINKER:--wrap,json_vsprintf + LINKER:--wrap,json_sprintf ) add_library(Celix::jansson_ei ALIAS jansson_ei) diff --git a/libs/error_injector/jansson/include/jansson_ei.h b/libs/error_injector/jansson/include/jansson_ei.h index 60f04e45d..167ace857 100644 --- a/libs/error_injector/jansson/include/jansson_ei.h +++ b/libs/error_injector/jansson/include/jansson_ei.h @@ -34,6 +34,8 @@ CELIX_EI_DECLARE(json_array_append_new, int); CELIX_EI_DECLARE(json_integer, json_t*); CELIX_EI_DECLARE(json_string, json_t*); CELIX_EI_DECLARE(json_real, json_t*); +CELIX_EI_DECLARE(json_vsprintf,json_t*); +CELIX_EI_DECLARE(json_sprintf, json_t*); #ifdef __cplusplus } diff --git a/libs/error_injector/jansson/src/jansson_ei.cc b/libs/error_injector/jansson/src/jansson_ei.cc index 57033c0c4..1d820629e 100644 --- a/libs/error_injector/jansson/src/jansson_ei.cc +++ b/libs/error_injector/jansson/src/jansson_ei.cc @@ -23,69 +23,87 @@ extern "C" { -size_t __real_json_array_size(const json_t *array); +size_t __real_json_array_size(const json_t* array); CELIX_EI_DEFINE(json_array_size, size_t) -size_t __wrap_json_array_size(const json_t *array) { +size_t __wrap_json_array_size(const json_t* array) { CELIX_EI_IMPL(json_array_size); return __real_json_array_size(array); } -char *__real_json_dumps(const json_t *json, size_t flags); +char* __real_json_dumps(const json_t* json, size_t flags); CELIX_EI_DEFINE(json_dumps, char*) -char *__wrap_json_dumps(const json_t *json, size_t flags) { +char* __wrap_json_dumps(const json_t* json, size_t flags) { CELIX_EI_IMPL(json_dumps); return __real_json_dumps(json, flags); } -json_t *__real_json_object(void); +json_t* __real_json_object(void); CELIX_EI_DEFINE(json_object, json_t*) -json_t *__wrap_json_object(void) { +json_t* __wrap_json_object(void) { CELIX_EI_IMPL(json_object); return __real_json_object(); } -int __real_json_object_set_new(json_t *object, const char *key, json_t *value); +int __real_json_object_set_new(json_t* object, const char* key, json_t* value); CELIX_EI_DEFINE(json_object_set_new, int) -int __wrap_json_object_set_new(json_t *object, const char *key, json_t *value) { - json_auto_t *val = value; +int __wrap_json_object_set_new(json_t* object, const char* key, json_t* value) { + json_auto_t* val = value; CELIX_EI_IMPL(json_object_set_new); return __real_json_object_set_new(object, key, celix_steal_ptr(val)); } -json_t *__real_json_array(void); +json_t* __real_json_array(void); CELIX_EI_DEFINE(json_array, json_t*) -json_t *__wrap_json_array(void) { +json_t* __wrap_json_array(void) { CELIX_EI_IMPL(json_array); return __real_json_array(); } -int __real_json_array_append_new(json_t *array, json_t *value); +int __real_json_array_append_new(json_t* array, json_t* value); CELIX_EI_DEFINE(json_array_append_new, int) -int __wrap_json_array_append_new(json_t *array, json_t *value) { - json_auto_t *val = value; +int __wrap_json_array_append_new(json_t* array, json_t* value) { + json_auto_t* val = value; CELIX_EI_IMPL(json_array_append_new); return __real_json_array_append_new(array, celix_steal_ptr(val)); } -json_t *__real_json_integer(json_int_t value); +json_t* __real_json_integer(json_int_t value); CELIX_EI_DEFINE(json_integer, json_t*) -json_t *__wrap_json_integer(json_int_t value) { +json_t* __wrap_json_integer(json_int_t value) { CELIX_EI_IMPL(json_integer); return __real_json_integer(value); } -json_t *__real_json_string(const char *value); +json_t* __real_json_string(const char* value); CELIX_EI_DEFINE(json_string, json_t*) -json_t *__wrap_json_string(const char *value) { +json_t* __wrap_json_string(const char* value) { CELIX_EI_IMPL(json_string); return __real_json_string(value); } -json_t *__real_json_real(double value); +json_t* __real_json_real(double value); CELIX_EI_DEFINE(json_real, json_t*) -json_t *__wrap_json_real(double value) { +json_t* __wrap_json_real(double value) { CELIX_EI_IMPL(json_real); return __real_json_real(value); } +json_t* __real_json_vsprintf(const char* fmt, va_list ap); +CELIX_EI_DEFINE(json_vsprintf, json_t*) +json_t* __wrap_json_vsprintf(const char* fmt, va_list ap) { + CELIX_EI_IMPL(json_vsprintf); + return __real_json_vsprintf(fmt, ap); +} + +json_t* __real_json_sprintf(const char* fmt, ...); +CELIX_EI_DEFINE(json_sprintf, json_t*) +json_t* __wrap_json_sprintf(const char* fmt, ...) { + CELIX_EI_IMPL(json_sprintf); + va_list args; + va_start(args, fmt); + json_t* obj = __real_json_vsprintf(fmt, args); + va_end(args); + return obj; +} + } \ No newline at end of file diff --git a/libs/utils/error_injector/celix_version/CMakeLists.txt b/libs/utils/error_injector/celix_version/CMakeLists.txt index ed7aadcdb..81886a5f8 100644 --- a/libs/utils/error_injector/celix_version/CMakeLists.txt +++ b/libs/utils/error_injector/celix_version/CMakeLists.txt @@ -24,5 +24,6 @@ target_link_options(version_ei INTERFACE LINKER:--wrap,celix_version_createVersionFromString LINKER:--wrap,celix_version_parse LINKER:--wrap,celix_version_copy + LINKER:--wrap,celix_version_toString ) add_library(Celix::version_ei ALIAS version_ei) diff --git a/libs/utils/error_injector/celix_version/include/celix_version_ei.h b/libs/utils/error_injector/celix_version/include/celix_version_ei.h index a82318574..e5d510f74 100644 --- a/libs/utils/error_injector/celix_version/include/celix_version_ei.h +++ b/libs/utils/error_injector/celix_version/include/celix_version_ei.h @@ -31,6 +31,8 @@ CELIX_EI_DECLARE(celix_version_parse, celix_status_t); CELIX_EI_DECLARE(celix_version_copy, celix_version_t*); +CELIX_EI_DECLARE(celix_version_toString, char*); + #ifdef __cplusplus } #endif diff --git a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc index eefad50d3..ddcdf9f81 100644 --- a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc +++ b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc @@ -41,4 +41,11 @@ celix_version_t* __wrap_celix_version_copy(const celix_version_t* version) { return __real_celix_version_copy(version); } +char* __real_celix_version_toString(const celix_version_t* version); +CELIX_EI_DEFINE(celix_version_toString, char*); +char* __wrap_celix_version_toString(const celix_version_t* version) { + CELIX_EI_IMPL(celix_version_toString); + return __real_celix_version_toString(version); +} + } \ No newline at end of file diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt index 11c6ec00c..08be2b58f 100644 --- a/libs/utils/gtest/CMakeLists.txt +++ b/libs/utils/gtest/CMakeLists.txt @@ -127,6 +127,7 @@ if (EI_TESTS) src/VersionErrorInjectionTestSuite.cc src/HashMapErrorInjectionTestSuite.cc src/FilterErrorInjectionTestSuite.cc + src/PropertiesEncodingErrorInjectionTestSuite.cc ) target_link_libraries(test_utils_with_ei PRIVATE Celix::zip_ei @@ -143,6 +144,7 @@ if (EI_TESTS) Celix::long_hash_map_ei Celix::version_ei Celix::array_list_ei + Celix::jansson_ei GTest::gtest GTest::gtest_main ) target_include_directories(test_utils_with_ei PRIVATE ../src) #for version_private (needs refactoring of test) diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc new file mode 100644 index 000000000..9e2d3bc30 --- /dev/null +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -0,0 +1,305 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix_err.h" +#include "celix_properties.h" + +#include "celix_array_list_ei.h" +#include "celix_utils_ei.h" +#include "celix_version_ei.h" +#include "jansson_ei.h" +#include "malloc_ei.h" +#include "stdio_ei.h" + +class PropertiesEncodingErrorInjectionTestSuite : public ::testing::Test { + public: + PropertiesEncodingErrorInjectionTestSuite() = default; + + ~PropertiesEncodingErrorInjectionTestSuite() override { + celix_ei_expect_json_object(nullptr, 0, nullptr); + celix_ei_expect_open_memstream(nullptr, 0, nullptr); + celix_ei_expect_celix_utils_writeOrCreateString(nullptr, 0, nullptr); + celix_ei_expect_json_object_set_new(nullptr, 0, -1); + celix_ei_expect_json_sprintf(nullptr, 0, nullptr); + celix_ei_expect_celix_version_toString(nullptr, 0, nullptr); + celix_ei_expect_malloc(nullptr, 0, nullptr); + celix_ei_expect_celix_arrayList_createWithOptions(nullptr, 0, nullptr); + celix_ei_expect_celix_arrayList_addString(nullptr, 0, CELIX_SUCCESS); + + } +}; + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveErrorTest) { + //Given a dummy properties object + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key", "value"); + + //When an error injected is prepared for json_object() from saveToStream + celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr); + + //And a dummy stream is created + FILE* stream = fopen("/dev/null", "w"); + + //When I call celix_properties_saveToStream + celix_status_t status = celix_properties_saveToStream(props, stream, 0); + + //Then I expect an error + EXPECT_EQ(CELIX_ENOMEM, status); + fclose(stream); + + //When an error injected is prepared for open_memstream()n from save + celix_ei_expect_open_memstream((void*)celix_properties_saveToString, 0, nullptr); + + //When I call celix_properties_saveToString + char* out = nullptr; + status = celix_properties_saveToString(props, 0, &out); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + //And I expect 2 error messages in celix_err + EXPECT_EQ(2, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { + // Given a dummy properties object + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key/with/slash", "value"); + celix_properties_set(props, "key-with-out-slash", "value"); + + // When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_saveToString + celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_saveToString, 2, nullptr); + + // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) + char* out = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // When an error injected is prepared for json_object() from celix_properties_saveToString + celix_ei_expect_json_object((void*)celix_properties_saveToString, 2, nullptr); + + // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // When an error injected is prepared for json_object_set_new() from celix_properties_saveToString + celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 2, -1); + + // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // When an error injected is prepared for json_string() from celix_properties_saveToString + celix_ei_expect_json_string((void*)celix_properties_saveToString, 3, nullptr); + + // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // When an error injected is prepared for json_object_set_new() from celix_properties_saveToString + celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 3, -1); + + // And I call celix_properties_saveToString using FLAT encoding (whitebox-knowledge) + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_FLAT, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // And I expect 5 error message in celix_err + EXPECT_EQ(5, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeArrayErrorTest) { + // Given a dummy properties object with an array + celix_autoptr(celix_properties_t) props = celix_properties_create(); + auto* arr = celix_arrayList_createStringArray(); + celix_arrayList_addString(arr, "value1"); + celix_arrayList_addString(arr, "value2"); + celix_properties_assignArrayList(props, "key", arr); + + // When an error injected is prepared for json_array() from celix_properties_saveToString + celix_ei_expect_json_array((void*)celix_properties_saveToString, 4, nullptr); + + // And I call celix_properties_saveToString + char* out = nullptr; + auto status = celix_properties_saveToString(props, 0, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + + //When an error injected is prepared for json_array_append_new() from loadFromString2 + celix_ei_expect_json_array_append_new((void*)celix_properties_saveToString, 4, -1); + + //And I call celix_properties_saveToString + status = celix_properties_saveToString(props, 0, &out); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + //When an error injected is prepared for json_string() from loadFromString2 + celix_ei_expect_json_string((void*)celix_properties_saveToString, 5, nullptr); + + //And I call celix_properties_saveToString + status = celix_properties_saveToString(props, 0, &out); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // And I expect 3 error message in celix_err + EXPECT_EQ(3, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeVersionErrorTest) { + // Given a dummy properties object with a version + celix_autoptr(celix_properties_t) props = celix_properties_create(); + auto* version = celix_version_create(1, 2, 3, "qualifier"); + celix_properties_assignVersion(props, "key", version); + + // When an error injected is prepared for json_sprintf() from celix_properties_saveToString + celix_ei_expect_json_sprintf((void*)celix_properties_saveToString, 4, nullptr); + + // And I call celix_properties_saveToString + char* out = nullptr; + auto status = celix_properties_saveToString(props, 0, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // When an error injected is prepared for celix_version_toString() from celix_properties_saveToString + celix_ei_expect_celix_version_toString((void*)celix_properties_saveToString, 4, nullptr); + + // And I call celix_properties_saveToString + status = celix_properties_saveToString(props, 0, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // And I expect 2 error message in celix_err + EXPECT_EQ(2, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, LoadErrorTest) { + //Given a dummy json string + const char* json = R"({"key":"value"})"; + + //When an error injected is prepared for fmemopen() from loadFromString2 + celix_ei_expect_fmemopen((void*)celix_properties_loadFromString2, 0, nullptr); + + //When I call celix_properties_loadFromString + celix_properties_t* props; + auto status = celix_properties_loadFromString2(json, 0, &props); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + //And I expect 1 error message in celix_err + EXPECT_EQ(1, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeErrorTest) { + //Given a dummy json string + const char* json = R"({"key":"value", "object": {"key":"value"}})"; + + //When an error injected is prepared for celix_properties_create()->malloc() from celix_properties_loadFromString2 + celix_ei_expect_malloc((void*)celix_properties_loadFromString2, 3, nullptr); + + //When I call celix_properties_loadFromString + celix_properties_t* props; + auto status = celix_properties_loadFromString2(json, 0, &props); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + //When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_loadFromString2 + celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_loadFromString2, 3, nullptr); + + //When I call celix_properties_loadFromString + status = celix_properties_loadFromString2(json, 0, &props); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + //And I expect 2 error message in celix_err + EXPECT_EQ(2, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeArrayErrorTest) { + //Given a dummy json string + const char* json = R"({"key":["value1", "value2"]})"; + + // When an error injected is prepared for celix_arrayList_createWithOptions() from celix_properties_loadFromString2 + celix_ei_expect_celix_arrayList_createWithOptions((void*)celix_properties_loadFromString2, 4, nullptr); + + //When I call celix_properties_loadFromString + celix_properties_t* props; + auto status = celix_properties_loadFromString2(json, 0, &props); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // When an error injected is prepared for celix_arrayList_addString() from celix_properties_loadFromString2 + celix_ei_expect_celix_arrayList_addString((void*)celix_properties_loadFromString2, 4, ENOMEM); + + //When I call celix_properties_loadFromString + status = celix_properties_loadFromString2(json, 0, &props); + + //Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // And I expect 0 error message in celix_err. Note because errors are injected for celix_array_list_t, celix_err is + // not used + EXPECT_EQ(0, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeVersionErrorTest) { + // Given a dummy json version string + const char* json = R"({"key":"version<1.2.3.qualifier>"})"; + + // When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_loadFromString2 + celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_loadFromString2, 4, nullptr); + + // And I call celix_properties_loadFromString + celix_properties_t* props; + auto status = celix_properties_loadFromString2(json, 0, &props); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // And I expect 1 error message in celix_err + EXPECT_EQ(1, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index b854e1831..bece2147a 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -17,16 +17,14 @@ * under the License. */ -#include #include +#include #include #include "celix_err.h" #include "celix_properties.h" #include "celix_stdlib_cleanup.h" -using ::testing::MatchesRegex; - class PropertiesSerializationTestSuite : public ::testing::Test { public: PropertiesSerializationTestSuite() { celix_err_resetErrors(); } @@ -189,7 +187,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { //And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); } TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { @@ -349,7 +347,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) { //And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); } TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithAndWithoutStrictFlagTest) { @@ -373,7 +371,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithAndWithoutStrictFlagT //And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); } TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithPrettyPrintTest) { @@ -410,7 +408,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveWithInvalidStreamTest) { EXPECT_EQ(2, celix_err_getErrorCount()); fclose(readStream); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); } TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { @@ -563,11 +561,11 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) { auto status = celix_properties_loadFromStream(stream, 0, &props); //Then loading fails - EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + EXPECT_NE(CELIX_SUCCESS, status); //And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); fclose(stream); } @@ -661,7 +659,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) { // And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); } TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { @@ -713,7 +711,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { // And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); // When decoding the properties from a string using a flag that allows collisions celix_autoptr(celix_properties_t) props3 = nullptr; @@ -724,7 +722,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { // And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); } TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagTest) { @@ -770,7 +768,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagT //And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); fclose(stream); } @@ -795,7 +793,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSlashesInTheKeysTest) // When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; auto status = celix_properties_loadFromStream(stream, 0, &props); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); ASSERT_EQ(CELIX_SUCCESS, status); // Then the properties object contains the nested objects @@ -821,7 +819,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidVersionsTest) // And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); // Given a JSON object with an invalid version strings, that are not recognized as versions jsonInput = @@ -857,7 +855,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadWithInvalidStreamTest) { fclose(stream); free(buf); - celix_err_printErrors(stderr, "Error: ", "\n"); + celix_err_printErrors(stderr, "Test Error: ", "\n"); } TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) { diff --git a/libs/utils/src/properties.c b/libs/utils/src/properties.c index 024d849db..10787e230 100644 --- a/libs/utils/src/properties.c +++ b/libs/utils/src/properties.c @@ -334,6 +334,8 @@ celix_properties_t* celix_properties_create() { free(props); props = NULL; } + } else { + celix_err_push("Cannot allocate memory for properties"); } return props; } @@ -451,7 +453,6 @@ celix_properties_t* celix_properties_loadWithStream(FILE* file) { celix_autoptr(celix_properties_t) props = celix_properties_create(); if (!props) { - celix_err_push("Failed to create properties"); return NULL; } diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index 087590f76..0b5f189c9 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -35,12 +35,12 @@ static celix_status_t celix_properties_versionToJson(const celix_version_t* vers celix_autofree char* versionStr = celix_version_toString(version); if (!versionStr) { celix_err_push("Failed to create version string."); - return CELIX_ENOMEM; + return ENOMEM; } *out = json_sprintf("version<%s>", versionStr); if (!*out) { celix_err_push("Failed to create json string."); - return CELIX_ENOMEM; + return ENOMEM; } return CELIX_SUCCESS; } @@ -72,7 +72,7 @@ static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_ } if (!*out) { celix_err_push("Failed to create json value."); - return CELIX_ENOMEM; + return ENOMEM; } return CELIX_SUCCESS; } @@ -90,10 +90,10 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, return CELIX_SUCCESS; // empty array -> treat as unset property } - json_t* array = json_array(); + json_auto_t* array = json_array(); if (!array) { celix_err_push("Failed to create json array."); - return CELIX_ENOMEM; + return ENOMEM; } for (int i = 0; i < celix_arrayList_size(entry->typed.arrayValue); ++i) { @@ -102,7 +102,6 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, json_t* jsonValue; celix_status_t status = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry, &jsonValue); if (status != CELIX_SUCCESS) { - json_decref(array); return status; } else if (!jsonValue) { // ignore unset values @@ -110,13 +109,12 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, int rc = json_array_append_new(array, jsonValue); if (rc != 0) { celix_err_push("Failed to append json string to array."); - json_decref(array); - return CELIX_ENOMEM; + return ENOMEM; } } } - *out = array; + *out = celix_steal_ptr(array); return CELIX_SUCCESS; } @@ -146,14 +144,14 @@ celix_properties_entryValueToJson(const char* key, const celix_properties_entry_ return celix_properties_arrayEntryValueToJson(key, entry, flags, out); default: // LCOV_EXCL_START - celix_err_pushf("Invalid properties entry type %d.", entry->valueType); + celix_err_pushf("Unexpected properties entry type %d.", entry->valueType); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } if (!*out) { celix_err_pushf("Failed to create json value for key '%s'.", key); - return CELIX_ENOMEM; + return ENOMEM; } return CELIX_SUCCESS; } @@ -175,7 +173,7 @@ static celix_status_t celix_properties_addJsonValueToJson(json_t* value, const c int rc = json_object_set_new(obj, key, value); if (rc != 0) { celix_err_push("Failed to set json object"); - return CELIX_ENOMEM; + return ENOMEM; } return CELIX_SUCCESS; } @@ -205,7 +203,7 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, name); if (!name) { celix_err_push("Failed to create name string"); - return CELIX_ENOMEM; + return ENOMEM; } json_t* subObj = json_object_get(jsonObj, name); if (!subObj || !json_is_object(subObj)) { @@ -216,12 +214,12 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop subObj = json_object(); if (!subObj) { celix_err_push("Failed to create json object"); - return CELIX_ENOMEM; + return ENOMEM; } int rc = json_object_set_new(jsonObj, name, subObj); if (rc != 0) { celix_err_push("Failed to set json object"); - return CELIX_ENOMEM; + return ENOMEM; } } @@ -242,6 +240,7 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie json_t* root = json_object(); if (!root) { celix_err_push("Failed to create json object"); + return ENOMEM; } if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED)) { @@ -295,7 +294,7 @@ celix_status_t celix_properties_saveToString(const celix_properties_t* propertie FILE* stream = open_memstream(&buffer, &size); if (!stream) { celix_err_push("Failed to open memstream."); - return CELIX_FILE_IO_EXCEPTION; + return ENOMEM; } celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); @@ -310,13 +309,13 @@ static celix_status_t celix_properties_parseVersion(const char* value, celix_ver // precondition: value is a valid version string (8 chars prefix and 1 char suffix) *out = NULL;; char buf[32]; - char* corrected = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 9, value + 8); - celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buf, corrected); - if (!corrected) { - celix_err_push("Failed to create corrected version string."); + char* extracted = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 9, value + 8); + celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buf, extracted); + if (!extracted) { + celix_err_push("Failed to create extracted version string."); return ENOMEM; } - celix_status_t status = celix_version_parse(corrected, out); + celix_status_t status = celix_version_parse(extracted, out); if (status != CELIX_SUCCESS) { celix_err_push("Failed to parse version string."); return status; @@ -412,7 +411,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j opts.elementType = elType; celix_autoptr(celix_array_list_t) array = celix_arrayList_createWithOptions(&opts); if (!array) { - return CELIX_ENOMEM; + return ENOMEM; } json_t* value; @@ -439,7 +438,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j } default: // LCOV_EXCL_START - celix_err_pushf("Invalid array list element type %d for key %s.", elType, key); + celix_err_pushf("Unexpected array list element type %d for key %s.", elType, key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -488,7 +487,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, combinedKey); if (!combinedKey) { celix_err_push("Failed to create sub key."); - return CELIX_ENOMEM; + return ENOMEM; } status = celix_properties_decodeValue(props, combinedKey, fieldValue, flags); if (status != CELIX_SUCCESS) { @@ -514,7 +513,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* return CELIX_SUCCESS; } else { // LCOV_EXCL_START - celix_err_pushf("Invalid json value type for key '%s'.", key); + celix_err_pushf("Unexpected json value type for key '%s'.", key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -529,7 +528,7 @@ static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, ce celix_autoptr(celix_properties_t) props = celix_properties_create(); if (!props) { - return CELIX_ENOMEM; + return ENOMEM; } const char* key; @@ -574,7 +573,7 @@ celix_status_t celix_properties_loadFromString2(const char* input, int decodeFla FILE* stream = fmemopen((void*)input, strlen(input), "r"); if (!stream) { celix_err_push("Failed to open memstream."); - return CELIX_FILE_IO_EXCEPTION; + return ENOMEM; } celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out); fclose(stream); From 56c67673b6d4dc15ac710113719389656246241d Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Sun, 14 Apr 2024 19:34:09 +0200 Subject: [PATCH 10/25] gh-685: Separate version ei testing to prevent own ei code inteference --- libs/utils/gtest/CMakeLists.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt index 08be2b58f..48f820a66 100644 --- a/libs/utils/gtest/CMakeLists.txt +++ b/libs/utils/gtest/CMakeLists.txt @@ -105,6 +105,19 @@ if (EI_TESTS) add_test(NAME test_utils_celix_err_with_ei COMMAND test_utils_celix_err_with_ei) setup_target_for_coverage(test_utils_celix_err_with_ei SCAN_DIR ..) + #Note testing version seperated, otherwise version calls are already wrapped + add_executable(test_utils_version_with_ei + src/VersionErrorInjectionTestSuite.cc + ) + target_link_libraries(test_utils_version_with_ei PRIVATE + utils_cut + Celix::malloc_ei + Celix::asprintf_ei + Celix::utils_ei + GTest::gtest GTest::gtest_main + ) + add_test(NAME test_utils_version_with_ei COMMAND test_utils_version_with_ei) + setup_target_for_coverage(test_utils_version_with_ei SCAN_DIR ..) #Note testing array list seperated, otherwise array list calls are already wrapped add_executable(test_utils_array_list_with_ei @@ -124,7 +137,6 @@ if (EI_TESTS) src/FileUtilsErrorInjectionTestSuite.cc src/ConvertUtilsErrorInjectionTestSuite.cc src/PropertiesErrorInjectionTestSuite.cc - src/VersionErrorInjectionTestSuite.cc src/HashMapErrorInjectionTestSuite.cc src/FilterErrorInjectionTestSuite.cc src/PropertiesEncodingErrorInjectionTestSuite.cc From ac2b13197f5cc863e4dd9e341a71c7188db2a533 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Sun, 14 Apr 2024 19:48:16 +0200 Subject: [PATCH 11/25] gh-685: Refactor assert to prevent used var Also: Rename the nested / flat encoding style flag. --- conanfile.py | 2 +- ...opertiesEncodingErrorInjectionTestSuite.cc | 10 +- .../gtest/src/PropertiesEncodingTestSuite.cc | 10 +- libs/utils/include/celix_properties.h | 295 ++++++++++++++++-- libs/utils/src/properties_encoding.c | 11 +- 5 files changed, 283 insertions(+), 45 deletions(-) diff --git a/conanfile.py b/conanfile.py index 63d9ac005..d8b46d017 100644 --- a/conanfile.py +++ b/conanfile.py @@ -332,7 +332,7 @@ def requirements(self): self.requires("civetweb/1.16") if self.options.build_celix_dfi: self.requires("libffi/[>=3.2.1 <4.0.0]") - if self.option.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib: + if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib: self.requires("jansson/[>=2.12 <3.0.0]") if self.options.build_rsa_discovery_zeroconf: # TODO: To be replaced with mdnsresponder/1790.80.10, resolve some problems of mdnsresponder diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc index 9e2d3bc30..3bcc804e7 100644 --- a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -91,7 +91,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) char* out = nullptr; - auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out); // Then I expect an error EXPECT_EQ(ENOMEM, status); @@ -100,7 +100,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { celix_ei_expect_json_object((void*)celix_properties_saveToString, 2, nullptr); // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) - status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out); // Then I expect an error EXPECT_EQ(ENOMEM, status); @@ -109,7 +109,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 2, -1); // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) - status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out); // Then I expect an error EXPECT_EQ(ENOMEM, status); @@ -118,7 +118,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { celix_ei_expect_json_string((void*)celix_properties_saveToString, 3, nullptr); // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) - status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &out); + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out); // Then I expect an error EXPECT_EQ(ENOMEM, status); @@ -127,7 +127,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 3, -1); // And I call celix_properties_saveToString using FLAT encoding (whitebox-knowledge) - status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_FLAT, &out); + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_FLAT_STYLE, &out); // Then I expect an error EXPECT_EQ(ENOMEM, status); diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index bece2147a..d4ff8e171 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -206,7 +206,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { FILE* stream = open_memstream(&buf, &bufLen); //When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream, CELIX_PROPERTIES_ENCODE_NESTED); + auto status = celix_properties_saveToStream(props, stream, CELIX_PROPERTIES_ENCODE_NESTED_STYLE); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties @@ -238,7 +238,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { //When saving the properties to a string celix_autofree char* output = nullptr; - auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation of the properties with the collisions resolved @@ -269,7 +269,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTe //When saving the properties to a string char* output = nullptr; - auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output); ASSERT_EQ(CELIX_SUCCESS, status); //Then the out contains the JSON representation snippets of the properties @@ -330,7 +330,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) { //When saving the properties to a string char* output = nullptr; - auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output); //Then the save succeeds ASSERT_EQ(CELIX_SUCCESS, status); @@ -340,7 +340,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) { //When saving the properties to a string with the error on key collision flag status = celix_properties_saveToString( - props, CELIX_PROPERTIES_ENCODE_NESTED | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output); + props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output); //Then the save fails, because the keys collide ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index 04dd04f93..a483f26af 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -937,62 +937,301 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i !celix_propertiesIterator_isEnd(&(iterName)); \ celix_propertiesIterator_next(&(iterName))) +/** + * @brief Flag to indicate that the encoded output should be pretty; e.g. encoded with additional whitespaces, + * newlines and indentation. + * + * If this flag is not set, the encoded output will compact; e.g. without additional whitespaces, newlines and + * indentation. + */ +#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 +/** + * @brief Flag to indicate that the encoded output should be flat; e.g. all properties entries are written as top level + * field entries. + * + * E.g: + * @code{.c} + * celix_properties_t* properties = celix_properties_create(); + * celix_properties_setString(properties, "key/with/slash", "value1"); + * celix_properties_setString(properties, "key", "value2"); + * char* json; + * celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_FLAT, &json); + * // json will be: {"key/with/slash": "value1", "key": "value2"} + * @endcode + * + * Note that encoding with a flat encoding style, all properties keys are unique JSON keys and can be written. + * + * If no encoding style flag is set, the encoded output will use the default encoding style. + */ +#define CELIX_PROPERTIES_ENCODE_FLAT_STYLE 0x02 -//TODO document the encode flags -#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 -#define CELIX_PROPERTIES_ENCODE_FLAT 0x02 //TODO doc, explain that decoding options ensures all properties entries are written, but only as a top level field entries. -#define CELIX_PROPERTIES_ENCODE_NESTED 0x04 +/** + * @brief Flag to indicate that the encoded output should be nested; e.g. properties entries are split on '/' and nested + * in JSON objects. + * + * E.g: + * @code{.c} + * celix_properties_t* properties = celix_properties_create(); + * celix_properties_setString(properties, "key/with/slash", "value1"); + * celix_properties_setString(properties, "key", "value2"); + * char* json; + * celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_NESTED, &json); + * // json will be: {"key":{"with":{"slash": "value1"}}} + * // or + * // json will be: {"key": "value2"} + * @endcode + * + * Note that encoding with a nested encoding style, it properties key can collide resulting in missing properties + * entries or (if CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS is set) an error. + * + * If no encoding style flag is set, the encoded output will use the default encoding style. + */ +#define CELIX_PROPERTIES_ENCODE_NESTED_STYLE 0x04 -#define CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS 0x10 -#define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS 0x20 +/** + * @brief Flag to indicate that the encoding should fail if the JSON representation will contain colliding keys. + * + * Note that colliding keys can only occur when using the nested encoding style. + * + * E.g. the following will lead to an error: + * @code{.c} + * celix_properties_t* properties = celix_properties_create(); + * celix_properties_setString(properties, "key/with/slash", "value1"); + * celix_properties_setString(properties, "key", "value2"); //collision + * char* json; + * celix_status_t status = celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_NESTED, &json); + * // status will be CELIX_ILLEGAL_ARGUMENT and a error message will be logged to celix_err + * @endcode + * + * If this flag is set, the encoding will fail if the JSON representation will contain colliding keys and if this flag + * is not set, the encoding will not fail and the colliding keys will be ignored. + */ +#define CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS 0x10 + +/** + * @brief Flag to indicate that the encoding should fail if the JSON representation will contain empty arrays. + * + * Although empty arrays are valid in JSON, they cannot be decoded to a valid properties array entry and as such + * empty arrays properties entries are not encoded. + * + * If this flag is set, the encoding will fail if the JSON representation will contain empty arrays and if this flag + * is not set, the encoding will not fail and the empty arrays will be ignored. + */ +#define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS 0x20 + +/** + * @brief Flag to indicate that all encode "error on" flags should be set. + */ #define CELIX_PROPERTIES_ENCODE_STRICT \ (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) -//TODO doc -CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const celix_properties_t* properties, - const char* filename, - int encodeFlags); - -//TODO doc. Not encode does not reset or close the stream position. +/** + * @brief Save (encode) as a JSON representation to a stream. + * + * The stream is expected to be a valid stream and is not reset or closed by this function. + * + * TODO document the JSON format + * + * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation. + * The default encoding style is a compact and flat JSON representation. + * + * @param properties The properties object to encode. + * @param stream The stream to write the JSON representation of the properties object to. + * @param encodeFlags The flags to use when encoding the input string. + * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be + * encoded to a JSON representation and ENOMEM if there was not enough memory. + */ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags); -//TODO doc +/** + * @brief Save (encode) properties as a JSON representation to a file. + * + * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream + * + * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation. + * The default encoding style is a compact and flat JSON representation. + * + * @param[in] properties The properties object to encode. + * @param[in] filename The file to write the JSON representation of the properties object to. + * @param[in] encodeFlags The flags to use when encoding the input string. + * returned string using free. + * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be + * encoded to a JSON representation and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file + * could not be opened or written to. + */ +CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const celix_properties_t* properties, + const char* filename, + int encodeFlags); + +/** + * @brief Save (encode) properties as a JSON representation to a string. + * + * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream + * + * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation. + * The default encoding style is a compact and flat JSON representation. + * + * @param[in] properties The properties object to encode. + * @param[in] encodeFlags The flags to use when encoding the input string. + * @param[out] out The JSON string representation of the properties object. The caller is responsible for freeing the + * returned string using free. + * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be + * encoded to a JSON representation and ENOMEM if there was not enough memory. + */ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_properties_t* properties, int encodeFlags, char** out); +/** + * @brief Flag to indicate that the decoding should fail if the input contains duplicate JSON keys. + * + * E.g. `{"key": "value", "key": "value2"}` is a duplicate key. + * + * If this flag is set, the decoding will fail if the input contains a duplicate key and if this flag is not set, the + * decoding will not fail and the last entry will be used. + */ +#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 + +/** + * @brief Flag to indicate that the decoding should fail if the input contains entry that collide on property keys. + * + * E.g. `{"obj/key": "value", "obj": {"key": "value2"}}` is a collision. + * + * If this flag is set, the decoding will fail if the input contains a collision and if this flag is not set, the + * decoding will not fail and the last entry will be used. + */ +#define CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS 0x02 + +/** + * @brief Flag to indicate that the decoding should fail if the input contains null values. + * + * E.g. `{"key": null}` is a null value. + * + * If this flag is set, the decoding will fail if the input contains a null value and if this flag is not set, the + * decoding will not fail and the JSON null entry will be ignored. + */ +#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x04 + +/** + * @brief Flag to indicate that the decoding should fail if the input contains empty arrays. + * + * + * E.g. `{"key": []}` is an empty array. + * + * Note that empty arrays are valid in JSON, but not cannot be decoded to a valid properties array entry. + * + * If this flag is set, the decoding will fail if the input contains an empty array and if this flag is not set, the + * decoding will not fail and the JSON empty array entry will be ignored. + */ +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x08 -//TODO document the decode flags -#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS 0x02 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x04 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x08 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x10 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x20 +/** + * @brief Flag to indicate that the decoding should fail if the input contains empty keys. + * + * E.g. `{"": "value"}` is an empty key. + * + * Note that empty keys are valid in JSON and valid in properties, but not always desired. + * + * If this flag is set, the decoding will fail if the input contains an empty key and if this flag is not set, the + * decoding will not fail and the JSON empty key entry will be ignored. + */ +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x10 + +/** + * @brief Flag to indicate that the decoding should fail if the input contains mixed arrays. + * + * E.g. `{"key": ["value", 1]}` is a mixed array. + * + * Note that mixed arrays are valid in JSON, but not cannot be decoded to a valid properties array entry. + * + * If this flag is set, the decoding will fail if the input contains a mixed array and if this flag is not set, the + * decoding will not fail and the JSON mixed array entry will be ignored. + */ +#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x20 + +/** + * @brief Flag to indicate that the decoding should fail if the input contains any of the decode error flags. + * + * This flag is a combination of all decode error flags. + */ #define CELIX_PROPERTIES_DECODE_STRICT \ (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS | \ CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | \ CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) -//TODO doc. Note load2, because load is currently already used. Will be updated in the future. -CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename, - int decodeFlags, - celix_properties_t** out); - -//TODO doc. Note decode does not reset or close the stream position. +/** + * @brief Load properties from a stream. + * + * The stream is expected to be a valid readable stream and is not reset or closed by this function. + * The content of the stream is expected to be in the format of a JSON object. + * + * TODO describe allowed and disallowed JSON objects. + * + * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation. + * + * @param[in] stream The input stream to parse. + * @param[in] decodeFlags The flags to use when decoding the input string. + * @param[out] out The properties object that will be created from the input string. The caller is responsible for + * freeing the returned properties object using celix_properties_destroy. + * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be + * decoded to a properties object and ENOMEM if there was not enough memory. + */ CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, celix_properties_t** out); -//TODO doc. Note celix_properties_loadFromString2, because loadFromString is currently already used. Will be updated in the future. +/** + * @brief Load properties from a file. + * + * @warning The name if temporary and will be renamed to celix_properties_load2 in the future (when + * the current celix_properties_load is removed). + * + * The content of the filename file is expected to be in the format of a JSON object. + * For what can and cannot be parsed, see celix_properties_loadFromStream documentation. + * + * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation. + * + * If an error occurs, the error status is returned and a message is logged to celix_err. + * + * @param[in] filename The file to load the properties from. + * @param[in] decodeFlags The flags to use when decoding the input string. + * @param[out] out The properties object that will be created from the input string. The caller is responsible for + * freeing the returned properties object using celix_properties_destroy. + * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be + * decoded to a properties object and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file + * could not be opened. + */ +CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename, + int decodeFlags, + celix_properties_t** out); + +/** + * @brief Load properties from a string. + * + * @warning The name if temporary and will be renamed to celix_properties_loadFromString in the future (when + * the current celix_properties_loadFromString is removed). + * + * The input string is expected to be in the format of a JSON object. + * For what can and cannot be parsed, see celix_properties_loadFromStream documentation. + * + * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation. + * + * If an error occurs, the error status is returned and a message is logged to celix_err. + * + * @param[in] input The input string to parse. + * @param[in] decodeFlags The flags to use when decoding the input string. + * @param[out] out The properties object that will be created from the input string. The caller is responsible for + * freeing the returned properties object using celix_properties_destroy. + * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be + * decoded to a properties object and ENOMEM if there was not enough memory. + */ CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromString2(const char* input, int decodeFlags, celix_properties_t** out); - #ifdef __cplusplus } #endif diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index 0b5f189c9..2b5c693bd 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -243,17 +243,17 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie return ENOMEM; } - if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED)) { + if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE)) { //no encoding flags set, default to flat - encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT; + encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT_STYLE; } CELIX_PROPERTIES_ITERATE(properties, iter) { celix_status_t status; - if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) { + if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) { status = celix_properties_addPropertiesEntryFlatToJson(&iter.entry, iter.key, root, encodeFlags); } else { - assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED); + assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE); status = celix_properties_addPropertiesEntryToJson(&iter.entry, iter.key, root, encodeFlags); } if (status != CELIX_SUCCESS) { @@ -340,8 +340,7 @@ static bool celix_properties_isVersionString(const char* value) { */ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArray, celix_array_list_element_type_t* out) { - size_t size = json_array_size(jsonArray); - assert(size > 0); //precondition: size > 0 + assert(json_array_size(jsonArray) > 0); //precondition: size > 0 json_t* value; int index; From 0e5894419921ddbb5f4c4868cf6e7c99b463bfd2 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Sun, 14 Apr 2024 23:03:53 +0200 Subject: [PATCH 12/25] gh-685: Fix several memleaks in properties encoding --- ...opertiesEncodingErrorInjectionTestSuite.cc | 8 +- .../gtest/src/PropertiesEncodingTestSuite.cc | 107 ++++++++++-------- libs/utils/include/celix_properties.h | 52 ++++++++- libs/utils/src/properties_encoding.c | 23 ++-- 4 files changed, 126 insertions(+), 64 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc index 3bcc804e7..2553e0e96 100644 --- a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -69,7 +69,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveErrorTest) { celix_ei_expect_open_memstream((void*)celix_properties_saveToString, 0, nullptr); //When I call celix_properties_saveToString - char* out = nullptr; + char* out; status = celix_properties_saveToString(props, 0, &out); //Then I expect an error @@ -90,7 +90,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_saveToString, 2, nullptr); // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge) - char* out = nullptr; + char* out; auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out); // Then I expect an error @@ -149,7 +149,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeArrayErrorTest) { celix_ei_expect_json_array((void*)celix_properties_saveToString, 4, nullptr); // And I call celix_properties_saveToString - char* out = nullptr; + char* out; auto status = celix_properties_saveToString(props, 0, &out); // Then I expect an error @@ -189,7 +189,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeVersionErrorTest) { celix_ei_expect_json_sprintf((void*)celix_properties_saveToString, 4, nullptr); // And I call celix_properties_saveToString - char* out = nullptr; + char* out; auto status = celix_properties_saveToString(props, 0, &out); // Then I expect an error diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index d4ff8e171..0771aa3ce 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -93,14 +93,17 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_setDouble(props, key, strtod(key, nullptr)); - //And an in-memory stream - celix_autofree char* buf = nullptr; - size_t bufLen = 0; - FILE* stream = open_memstream(&buf, &bufLen); + // Then saving the properties to a string succeeds, but value is not added to the JSON (because JSON does not + // support NAN, INF and -INF) + celix_autofree char* output; + auto status = celix_properties_saveToString(props, 0, &output); + ASSERT_EQ(CELIX_SUCCESS, status); + EXPECT_STREQ("{}", output); - //Then saving the properties to the stream fails, because JSON does not support NAN, INF and -INF + //And saving the properties to a string with the flag CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF fails celix_err_resetErrors(); - auto status = celix_properties_saveToStream(props, stream, 0); + char* output2; + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, &output2); EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); //And an error msg is added to celix_err @@ -112,25 +115,30 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { // Given a properties object with array list values celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_array_list_t* list1 = celix_arrayList_createStringArray(); celix_arrayList_addString(list1, "value1"); celix_arrayList_addString(list1, "value2"); celix_properties_assignArrayList(props, "key1", list1); + celix_array_list_t* list2 = celix_arrayList_createLongArray(); celix_arrayList_addLong(list2, 1); celix_arrayList_addLong(list2, 2); celix_properties_assignArrayList(props, "key2", list2); + celix_array_list_t* list3 = celix_arrayList_createDoubleArray(); celix_arrayList_addDouble(list3, 1.0); celix_arrayList_addDouble(list3, 2.0); celix_properties_assignArrayList(props, "key3", list3); + celix_array_list_t* list4 = celix_arrayList_createBoolArray(); celix_arrayList_addBool(list4, true); celix_arrayList_addBool(list4, false); celix_properties_assignArrayList(props, "key4", list4); + celix_array_list_t* list5 = celix_arrayList_createVersionArray(); - celix_arrayList_addVersion(list5, celix_version_create(1, 2, 3, "qualifier")); - celix_arrayList_addVersion(list5, celix_version_create(4, 5, 6, "qualifier")); + celix_arrayList_assignVersion(list5, celix_version_create(1, 2, 3, "qualifier")); + celix_arrayList_assignVersion(list5, celix_version_create(4, 5, 6, "qualifier")); celix_properties_assignArrayList(props, "key5", list5); // And an in-memory stream @@ -170,17 +178,18 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { EXPECT_EQ(5, celix_properties_size(props)); //When saving the properties to a string - char* output = nullptr; - auto status = celix_properties_saveToString(props, 0, &output); + celix_autofree char* output1; + auto status = celix_properties_saveToString(props, 0, &output1); //Then the save went ok ASSERT_EQ(CELIX_SUCCESS, status); //And the output contains an empty JSON object, because empty arrays are treated as unset - EXPECT_STREQ("{}", output); + EXPECT_STREQ("{}", output1); //When saving the properties to a string with an error on empty array flag - status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output); + char* output2; + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output2); //Then the save fails, because the empty array generates an error ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -268,7 +277,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTe //When saving the properties to a string - char* output = nullptr; + celix_autofree char* output; auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output); ASSERT_EQ(CELIX_SUCCESS, status); @@ -329,18 +338,19 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) { celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2" -> overwrite //When saving the properties to a string - char* output = nullptr; - auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output); + celix_autofree char* output1; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output1); //Then the save succeeds ASSERT_EQ(CELIX_SUCCESS, status); // And both keys are serialized (one as a flat key) (flat key name is whitebox knowledge) - EXPECT_NE(nullptr, strstr(output, R"({"key1":{"key2":"value2"}})")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output1, R"({"key1":{"key2":"value2"}})")) << "JSON: " << output1; //When saving the properties to a string with the error on key collision flag + char* output2; status = celix_properties_saveToString( - props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output); + props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output2); //Then the save fails, because the keys collide ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -357,7 +367,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithAndWithoutStrictFlagT celix_properties_assignArrayList(props, "key1", list); //When saving the properties to a string without the strict flag - char* output = nullptr; + celix_autofree char* output; auto status = celix_properties_saveToString(props, 0, &output); //Then the save succeeds @@ -381,7 +391,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithPrettyPrintTest) { celix_properties_set(props, "key2", "value2"); //When saving the properties to a string with pretty print - char* output = nullptr; + celix_autofree char* output; auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_PRETTY, &output); //Then the save succeeds @@ -414,17 +424,14 @@ TEST_F(PropertiesSerializationTestSuite, SaveWithInvalidStreamTest) { TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { //Given an empty JSON object const char* json = "{}"; - FILE* stream = fmemopen((void*)json, strlen(json), "r"); //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_loadFromStream(stream, 0, &props); + auto status = celix_properties_loadFromString2(json, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object is empty EXPECT_EQ(0, celix_properties_size(props)); - - fclose(stream); } TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { @@ -586,7 +593,8 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithEmptyArrayTest) { EXPECT_EQ(0, celix_properties_size(props)); //When loading the properties from string with a strict flag - status = celix_properties_loadFromString2(inputJSON, CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, &props); + celix_properties_t* props2; + status = celix_properties_loadFromString2(inputJSON, CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, &props2); //Then loading fails, because the empty array generates an error ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -651,7 +659,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) { EXPECT_EQ(3, celix_properties_getLong(props, "key", 0)); // When decoding the properties from the stream using a flog that does not allow duplicates - celix_autoptr(celix_properties_t) props2 = nullptr; + celix_autoptr(celix_properties_t) props2; status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); // Then loading fails, because of a duplicate key @@ -687,7 +695,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { })"; // When loading the properties from a string. - celix_autoptr(celix_properties_t) props = nullptr; + celix_autoptr(celix_properties_t) props; auto status = celix_properties_loadFromString2(jsonInput, 0, &props); // Then loading succeeds @@ -703,7 +711,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { EXPECT_STREQ("value7", celix_properties_getString(props, "object3/key4")); // When decoding the properties from a string using a flag that allows duplicates - celix_autoptr(celix_properties_t) props2 = nullptr; + celix_autoptr(celix_properties_t) props2; status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); // Then loading fails, because of a duplicate key @@ -714,7 +722,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); // When decoding the properties from a string using a flag that allows collisions - celix_autoptr(celix_properties_t) props3 = nullptr; + celix_autoptr(celix_properties_t) props3; status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, &props3); // Then loading fails, because of a collision @@ -861,34 +869,41 @@ TEST_F(PropertiesSerializationTestSuite, LoadWithInvalidStreamTest) { TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) { // Given a properties object with all possible types (but no empty arrays) celix_autoptr(celix_properties_t) props = celix_properties_create(); - celix_properties_set(props, "strKey", "strValue"); - celix_properties_setLong(props, "longKey", 42); - celix_properties_setDouble(props, "doubleKey", 2.0); - celix_properties_setBool(props, "boolKey", true); - celix_properties_setVersion(props, "versionKey", celix_version_create(1, 2, 3, "qualifier")); - auto* strArr = celix_arrayList_createStringArray(); + + celix_properties_set(props, "single/strKey", "strValue"); + celix_properties_setLong(props, "single/longKey", 42); + celix_properties_setDouble(props, "single/doubleKey", 2.0); + celix_properties_setBool(props, "single/boolKey", true); + celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier")); + + celix_array_list_t* strArr = celix_arrayList_createStringArray(); celix_arrayList_addString(strArr, "value1"); celix_arrayList_addString(strArr, "value2"); - auto* longArr = celix_arrayList_createLongArray(); + celix_properties_assignArrayList(props, "array/stringArr", strArr); + + celix_array_list_t* longArr = celix_arrayList_createLongArray(); celix_arrayList_addLong(longArr, 1); celix_arrayList_addLong(longArr, 2); - celix_properties_assignArrayList(props, "longArr", longArr); - auto* doubleArr = celix_arrayList_createDoubleArray(); + celix_properties_assignArrayList(props, "array/longArr", longArr); + + celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray(); celix_arrayList_addDouble(doubleArr, 1.0); celix_arrayList_addDouble(doubleArr, 2.0); - celix_properties_assignArrayList(props, "doubleArr", doubleArr); - auto* boolArr = celix_arrayList_createBoolArray(); + celix_properties_assignArrayList(props, "array/doubleArr", doubleArr); + + celix_array_list_t* boolArr = celix_arrayList_createBoolArray(); celix_arrayList_addBool(boolArr, true); celix_arrayList_addBool(boolArr, false); - celix_properties_assignArrayList(props, "boolArr", boolArr); - auto* versionArr = celix_arrayList_createVersionArray(); - celix_arrayList_addVersion(versionArr, celix_version_create(1, 2, 3, "qualifier")); - celix_arrayList_addVersion(versionArr, celix_version_create(4, 5, 6, "qualifier")); - celix_properties_assignArrayList(props, "versionArr", versionArr); + celix_properties_assignArrayList(props, "array/boolArr", boolArr); + + celix_array_list_t* versionArr = celix_arrayList_createVersionArray(); + celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier")); + celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier")); + celix_properties_assignArrayList(props, "array/versionArr", versionArr); // When saving the properties to a properties_test.json file const char* filename = "properties_test.json"; - auto status = celix_properties_save(props, filename, 0); + auto status = celix_properties_save(props, filename, CELIX_PROPERTIES_ENCODE_PRETTY); // Then saving succeeds ASSERT_EQ(CELIX_SUCCESS, status); @@ -902,4 +917,4 @@ TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) { // And the loaded properties are equal to the original properties EXPECT_TRUE(celix_properties_equals(props, loadedProps)); -} +} \ No newline at end of file diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index a483f26af..2ac692ea9 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -1020,6 +1020,16 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i */ #define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS 0x20 +/** + * @brief Flag to indicate that the encoding should fail if the JSON representation will contain NaN or Inf values. + * + * NaN, Inf and -Inf are not valid JSON values and as such properties entries with these values are not encoded. + * + * If this flag is set, the encoding will fail if the JSON representation will contain NaN or Inf values and if this + * flag is not set, the encoding will not fail and the NaN and Inf entries will be ignored. + */ +#define CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF 0x40 + /** * @brief Flag to indicate that all encode "error on" flags should be set. */ @@ -1031,7 +1041,26 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i * * The stream is expected to be a valid stream and is not reset or closed by this function. * - * TODO document the JSON format + * Properties are encoded as a JSON object. + * + * If no encoding style flag is set of when the CELIX_PROPERTIES_ENCODE_FLAT_STYLE flag is set, properties + * entries are written as top level field entries. + * + * If the CELIX_PROPERTIES_ENCODE_NESTED_STYLE flag is set, properties entry keys are split on '/' and nested in + * JSON objects. This leads to a more natural JSON representation, but if there are colliding properties keys (e.g. + * `{"key": "value1", "key/with/slash": "value2"}`), not all properties entries will be written. + * + * With all encoding styles, the empty array properties entries are ignored, because they cannot be decoded to a valid + * properties array entry. + * + * Properties type entries are encoded as follows: + * - CELIX_PROPERTIES_TYPE_STRING: The value is encoded as a JSON string. + * - CELIX_PROPERTIES_TYPE_LONG: The value is encoded as a JSON number. + * - CELIX_PROPERTIES_TYPE_DOUBLE: The value is encoded as a JSON number. + * - CELIX_PROPERTIES_TYPE_BOOL: The value is encoded as a JSON boolean. + * - CELIX_PROPERTIES_TYPE_ARRAY: The value is encoded as a JSON array, with each element encoded according to its type. + * - CELIX_PROPERTIES_TYPE_VERSION: The value is encoded as a JSON string with a "version<" prefix and a ">" suffix + * (e.g. "version<1.2.3>"). * * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation. * The default encoding style is a compact and flat JSON representation. @@ -1168,7 +1197,26 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_prop * The stream is expected to be a valid readable stream and is not reset or closed by this function. * The content of the stream is expected to be in the format of a JSON object. * - * TODO describe allowed and disallowed JSON objects. + * For decoding a single JSON object is decoded to a properties object. + * + * The keys of the JSON object are used as + * properties keys and the values of the JSON object are used as properties values. If there are nested + * JSON objects, the keys are concatenated with a '/' separator (e.g. `{"key": {"nested": "value"}}` will be + * decoded to a properties object with a single entry with key `key/nested` and (string) value `value`). + * + * Because properties keys are created by concatenating the JSON keys, there there could be collisions + * (e.g. `{"obj/key": "value", "obj": {"key": "value2"}}`, two entries with the key `obj/key`. In this case + * the last decoded JSON entry will be used. + * + * Properties entry types are determined by the JSON value type: + * - JSON string values are decoded as string properties entries. + * - JSON number values are decoded as long or double properties entries, depending on the value. + * - JSON boolean values are decoded as boolean properties entries. + * - jSON string values with a "version<" prefix and a ">" suffix are decoded as version properties entries (e.g. + * "version<1.2.3>"). + * - JSON array values are decoded as array properties entries. The array can contain any of the above types, but mixed + * arrays are not supported. + * - JSON null values are ignored. * * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation. * diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index 2b5c693bd..eba392079 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -130,8 +130,11 @@ celix_properties_entryValueToJson(const char* key, const celix_properties_entry_ break; case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE: if (isnan(entry->typed.doubleValue) || isinf(entry->typed.doubleValue)) { - celix_err_pushf("Invalid NaN or Inf in key '%s'.", key); - return CELIX_ILLEGAL_ARGUMENT; + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) { + celix_err_pushf("Invalid NaN or Inf in key '%s'.", key); + return CELIX_ILLEGAL_ARGUMENT; + } + return CELIX_SUCCESS; // ignore NaN and Inf } *out = json_real(entry->typed.doubleValue); break; @@ -184,10 +187,8 @@ static celix_status_t celix_properties_addPropertiesEntryFlatToJson(const celix_ int flags) { json_t* value; celix_status_t status = celix_properties_entryValueToJson(key, entry, flags, &value); - if (status != CELIX_SUCCESS) { - return status; - } - return celix_properties_addJsonValueToJson(value, key, root, flags); + status = CELIX_DO_IF(status, celix_properties_addJsonValueToJson(value, key, root, flags)); + return status; } static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_properties_entry_t* entry, @@ -237,7 +238,7 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop } celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) { - json_t* root = json_object(); + json_auto_t* root = json_object(); if (!root) { celix_err_push("Failed to create json object"); return ENOMEM; @@ -257,7 +258,6 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie status = celix_properties_addPropertiesEntryToJson(&iter.entry, iter.key, root, encodeFlags); } if (status != CELIX_SUCCESS) { - json_decref(root); return status; } } @@ -268,7 +268,6 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie } int rc = json_dumpf(root, stream, jsonFlags); - json_decref(root); if (rc != 0) { celix_err_push("Failed to dump json object to stream."); return CELIX_FILE_IO_EXCEPTION; @@ -432,7 +431,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: { celix_version_t* v; status = celix_properties_parseVersion(json_string_value(value), &v); - status = CELIX_DO_IF(status, celix_arrayList_addVersion(array, v)); + status = CELIX_DO_IF(status, celix_arrayList_assignVersion(array, v)); break; } default: @@ -468,7 +467,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* if (json_is_string(jsonValue) && celix_properties_isVersionString(json_string_value(jsonValue))) { celix_version_t* version; status = celix_properties_parseVersion(json_string_value(jsonValue), &version); - status = CELIX_DO_IF(status, celix_properties_setVersion(props, key, version)); + status = CELIX_DO_IF(status, celix_properties_assignVersion(props, key, version)); } else if (json_is_string(jsonValue)) { status = celix_properties_setString(props, key, json_string_value(jsonValue)); } else if (json_is_integer(jsonValue)) { @@ -549,7 +548,7 @@ celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, ce if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) { jsonFlags = JSON_REJECT_DUPLICATES; } - json_t* root = json_loadf(stream, jsonFlags, &jsonError); + json_auto_t* root = json_loadf(stream, jsonFlags, &jsonError); if (!root) { celix_err_pushf("Failed to parse json: %s.", jsonError.text); return CELIX_ILLEGAL_ARGUMENT; From ae4464ca46775eb69978d8bfc0de20564b5c553a Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 15 Apr 2024 19:17:17 +0200 Subject: [PATCH 13/25] gh-685: Fix incorrect celix_autoptr usage --- libs/utils/gtest/src/PropertiesEncodingTestSuite.cc | 6 +++--- libs/utils/src/properties_encoding.c | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 0771aa3ce..1b0035c09 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -659,7 +659,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) { EXPECT_EQ(3, celix_properties_getLong(props, "key", 0)); // When decoding the properties from the stream using a flog that does not allow duplicates - celix_autoptr(celix_properties_t) props2; + celix_properties_t* props2; status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); // Then loading fails, because of a duplicate key @@ -711,7 +711,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { EXPECT_STREQ("value7", celix_properties_getString(props, "object3/key4")); // When decoding the properties from a string using a flag that allows duplicates - celix_autoptr(celix_properties_t) props2; + celix_properties_t* props2; status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); // Then loading fails, because of a duplicate key @@ -722,7 +722,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); // When decoding the properties from a string using a flag that allows collisions - celix_autoptr(celix_properties_t) props3; + celix_properties_t* props3; status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, &props3); // Then loading fails, because of a collision diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index eba392079..74103f484 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -519,6 +519,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* } static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, celix_properties_t** out) { + *out = NULL; if (!json_is_object(obj)) { celix_err_push("Expected json object."); return CELIX_ILLEGAL_ARGUMENT; From a4814f3f4955fe0b260e2fc416849ef1eb1d82a0 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 15 Apr 2024 23:05:01 +0200 Subject: [PATCH 14/25] gh-685: Fix incorrect celix_autoptr usage --- .../gtest/src/PropertiesEncodingTestSuite.cc | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 1b0035c09..3e03b77ef 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -210,27 +210,23 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { celix_properties_set(props, "object3/object4/key6", "value6"); //And an in-memory stream - celix_autofree char* buf = nullptr; - size_t bufLen = 0; - FILE* stream = open_memstream(&buf, &bufLen); + celix_autofree char* output; //When saving the properties to the stream - auto status = celix_properties_saveToStream(props, stream, CELIX_PROPERTIES_ENCODE_NESTED_STYLE); + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties - fclose(stream); - EXPECT_NE(nullptr, strstr(buf, R"("key1":"value1")")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key2":"value2")")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("object1":{"key3":"value3","key4":"value4"})")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("object2":{"key5":"value5"})")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("object3":{"object4":{"key6":"value6"}})")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(output, R"("key1":"value1")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("key2":"value2")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("object1":{"key3":"value3","key4":"value4"})")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("object2":{"key5":"value5"})")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("object3":{"object4":{"key6":"value6"}})")) << "JSON: " << output; //And the buf is a valid JSON object json_error_t error; - json_t* root = json_loads(buf, 0, &error); + json_auto_t* root = json_loads(output, 0, &error); EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; - json_decref(root); } TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { @@ -374,7 +370,8 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithAndWithoutStrictFlagT ASSERT_EQ(CELIX_SUCCESS, status); //When saving the properties to a string with the strict flag - status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_STRICT, &output); + char* output2; + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_STRICT, &output2); //Then the save fails, because the empty array generates an error ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); From aa29987def9709f35b569280d61a2dd0aead5d12 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 15 Apr 2024 23:17:21 +0200 Subject: [PATCH 15/25] gh-685: Add properties encoding documentation --- documents/README.md | 1 + documents/properties_encoding.md | 333 +++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 documents/properties_encoding.md diff --git a/documents/README.md b/documents/README.md index 3930498a4..c174acbe7 100644 --- a/documents/README.md +++ b/documents/README.md @@ -86,6 +86,7 @@ bundles contains binaries depending on the stdlibc++ library. * [Apache Celix C Patterns](c_patterns.md) * Utils * [Apache Celix Properties & Filter](properties_and_filter.md) + * [Apache Celix Properties Encoding](properties_encoding.md) * Framework * [Apache Celix Bundles](bundles.md) * [Apache Celix Services](services.md) diff --git a/documents/properties_encoding.md b/documents/properties_encoding.md new file mode 100644 index 000000000..c4cdb38d7 --- /dev/null +++ b/documents/properties_encoding.md @@ -0,0 +1,333 @@ +--- +title: Apache Celix Properties Encoding +--- + + + +# Apache Celix Properties JSON Encoding + +## Introduction + +In Apache Celix, properties represent key-value pairs, often used for configuration. While these properties are not JSON +objects inherently, they can be encoded to and decoded from JSON for interoperability or storage. This page explains how +Apache Celix properties are encoded to and decoded from JSON. + +### Encoding limitations + +Except for empty arrays and the double values NaN, Infinity, and -Infinity, all Apache Celix properties types can +be encoded to JSON. + +The reason for the empty array limitation is that for a properties array entry the array list element type is must be +known, this is not possible to infer from an empty JSON array. To ensure that everything this is encoded, can be decoded +again, a properties array entry with an empty array is not encoded to JSON. + +The reason for the double values NaN, Infinity, and -Infinity limitation is that JSON does not support these values. + +### Decoding limitations + +When decoding JSON to Apache Celix properties, the following limitations apply: + +- Mixed array types are not supported. For example, an array with both strings and longs cannot be decoded to a + properties' entry. +- null values are not supported, because properties does not support a null value type. +- Empty arrays are not supported, because the array list element type must be known, this is not possible to infer from + an empty JSON array. +- JSON keys that collide on the created properties' key level are not supported. + See [Properties Decoding](##Properties Decoding) for more information. + +## Properties Encoding + +Apache Celix properties can be encoded to JSON using the `celix_properties_save`, `celix_properties_saveToStream` +and `celix_properties_saveToString` functions. These functions take a properties object and encode it to a JSON object +string. The encoding can be controlled using flags and can be done in a flat or nested structure. + +### Properties Flat Encoding + +By default, the encoding is done in a flat structure, because a flat structure ensures that all keys of the properties +can be represented in JSON format. When properties are encoded to JSON in a flat structure, the reverse operation, +decoding JSON that has been encoded from properties, will result in the same properties (except for the previously +mentioned limitations (empty arrays and the double values NaN, Infinity, and -Infinity)). + +Flat Encoding example: + +```C +#include +#include + +int main() { + celix_autoptr(celix_properties_t) props = celix_properties_create(); + + celix_properties_set(props, "single/strKey", "strValue"); + celix_properties_setLong(props, "single/longKey", 42); + celix_properties_setDouble(props, "single/doubleKey", 2.0); + celix_properties_setBool(props, "single/boolKey", true); + celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier")); + + celix_array_list_t* strArr = celix_arrayList_createStringArray(); + celix_arrayList_addString(strArr, "value1"); + celix_arrayList_addString(strArr, "value2"); + celix_properties_assignArrayList(props, "array/stringArr", strArr); + + celix_array_list_t* longArr = celix_arrayList_createLongArray(); + celix_arrayList_addLong(longArr, 1); + celix_arrayList_addLong(longArr, 2); + celix_properties_assignArrayList(props, "array/longArr", longArr); + + celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray(); + celix_arrayList_addDouble(doubleArr, 1.0); + celix_arrayList_addDouble(doubleArr, 2.0); + celix_properties_assignArrayList(props, "array/doubleArr", doubleArr); + + celix_array_list_t* boolArr = celix_arrayList_createBoolArray(); + celix_arrayList_addBool(boolArr, true); + celix_arrayList_addBool(boolArr, false); + celix_properties_assignArrayList(props, "array/boolArr", boolArr); + + celix_array_list_t* versionArr = celix_arrayList_createVersionArray(); + celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier")); + celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier")); + celix_properties_assignArrayList(props, "array/versionArr", versionArr); + + celix_properties_saveToStream(props, stdout, CELIX_PROPERTIES_ENCODE_PRETTY); +} +``` + +Will output the following JSON (order of keys can differ): + +```JSON +{ + "array/doubleArr": [ + 1.0, + 2.0 + ], + "array/boolArr": [ + true, + false + ], + "single/versionKey": "version<1.2.3.qualifier>", + "array/longArr": [ + 1, + 2 + ], + "single/strKey": "strValue", + "single/doubleKey": 2.0, + "single/boolKey": true, + "array/versionArr": [ + "version<1.2.3.qualifier>", + "version<4.5.6.qualifier>" + ], + "array/stringArr": [ + "value1", + "value2" + ], + "single/longKey": 42 +} +``` + +### Properties Nested Encoding + +When properties are encoded to JSON in a nested structure, the keys of the properties are used to create a nested JSON +object. This is done by using the '/' character in the properties key to create a nested JSON objects. When encoding +properties using a nested structure, there is a risk of key collisions. To detect key collisions, the flag +`CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS` can be used. + +Nested Encoding example: + +```C +#include +#include + +int main() { + celix_autoptr(celix_properties_t) props = celix_properties_create(); + + celix_properties_set(props, "single/strKey", "strValue"); + celix_properties_setLong(props, "single/longKey", 42); + celix_properties_setDouble(props, "single/doubleKey", 2.0); + celix_properties_setBool(props, "single/boolKey", true); + celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier")); + + celix_array_list_t* strArr = celix_arrayList_createStringArray(); + celix_arrayList_addString(strArr, "value1"); + celix_arrayList_addString(strArr, "value2"); + celix_properties_assignArrayList(props, "array/stringArr", strArr); + + celix_array_list_t* longArr = celix_arrayList_createLongArray(); + celix_arrayList_addLong(longArr, 1); + celix_arrayList_addLong(longArr, 2); + celix_properties_assignArrayList(props, "array/longArr", longArr); + + celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray(); + celix_arrayList_addDouble(doubleArr, 1.0); + celix_arrayList_addDouble(doubleArr, 2.0); + celix_properties_assignArrayList(props, "array/doubleArr", doubleArr); + + celix_array_list_t* boolArr = celix_arrayList_createBoolArray(); + celix_arrayList_addBool(boolArr, true); + celix_arrayList_addBool(boolArr, false); + celix_properties_assignArrayList(props, "array/boolArr", boolArr); + + celix_array_list_t* versionArr = celix_arrayList_createVersionArray(); + celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier")); + celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier")); + celix_properties_assignArrayList(props, "array/versionArr", versionArr); + + celix_properties_saveToStream(props, stdout, CELIX_PROPERTIES_ENCODE_PRETTY | CELIX_PROPERTIES_ENCODE_NESTED_STYLE); +} +``` + +Will output the following JSON (order of keys can differ): + +```JSON +{ + "array": { + "doubleArr": [ + 1.0, + 2.0 + ], + "boolArr": [ + true, + false + ], + "longArr": [ + 1, + 2 + ], + "versionArr": [ + "version<1.2.3.qualifier>", + "version<4.5.6.qualifier>" + ], + "stringArr": [ + "value1", + "value2" + ] + }, + "single": { + "versionKey": "version<1.2.3.qualifier>", + "strKey": "strValue", + "doubleKey": 2.0, + "boolKey": true, + "longKey": 42 + } +} +``` + +### Encoding Flags + +Properties encoding flags can be used control the behavior of the encoding. The following encoding flags can be used: + +- `CELIX_PROPERTIES_ENCODE_PRETTY`: Flag to indicate that the encoded output should be pretty; e.g. encoded with + additional whitespaces, newlines and indentation. If this flag is not set, the encoded output will compact; e.g. + without additional whitespaces, newlines and indentation. + +- `CELIX_PROPERTIES_ENCODE_FLAT_STYLE`: Flag to indicate that the encoded output should be flat; e.g. all properties + entries are written as top level field entries. + +- `CELIX_PROPERTIES_ENCODE_NESTED_STYLE`: Flag to indicate that the encoded output should be nested; e.g. properties + entries are split on '/' and nested in JSON objects. + +- `CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS`: Flag to indicate that the encoding should fail if the JSON + representation will contain colliding keys. Note that colliding keys can only occur when using the nested encoding + style. + +- `CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS`: Flag to indicate that the encoding should fail if the JSON + representation will contain empty arrays. + +- `CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF`: Flag to indicate that the encoding should fail if the JSON representation + will contain NaN or Inf values. + +- `CELIX_PROPERTIES_ENCODE_STRICT`: Flag to indicate that all encode "error on" flags should be set. + +## Properties Decoding + +JSON can be decoded to an Apache Celix properties object using +the `celix_properties_load2`, `celix_properties_loadFromStream` and `celix_properties_loadFromString2` functions. These +functions take a JSON input and decode it to a properties object. Because properties use a flat key structure, +decoding a nested JSON object to properties results in combining JSON object keys to a flat key structure. This can +result in key collisions. + +By default, the decoding will not fail on empty arrays, null values, empty keys, or mixed arrays and instead these JSON +entries will be ignored. Also by default, if decoding results in a duplicate properties key, the last value will be used +and no error will be returned. + +### Decoding example + +Given a `example.json` file with the following content: + +```JSON +{ + "counters": { + "counter1": 1, + "counter2": 2 + }, + "strings": { + "string1": "value1", + "string2": "value2" + } +} +``` + +Combined with the following code: + +```c +#include + +#include + +int main() { + celix_autoptr(celix_properties_t) props; + celix_status_t status = celix_properties_load2("example.json", 0, &props): + (void)status; //for production code check status + CELIX_PROPERTIES_ITERATE(props, iter) { + printf("key=%s, value=%s\n", celix_properties_key(iter.key), celix_properties_value(iter.entry.value)); + } +} +``` + +Will output the following: + +``` +key=counters/counter1, value=1 +key=counters/counter2, value=2 +key=strings/string1, value=value1 +key=strings/string2, value=value2 +``` + +### Decoding Flags + +Properties decoding behavior can be controlled using flags. The following decoding flags can be used: + +- `CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES`: Flag to indicate that the decoding should fail if the input contains + duplicate JSON keys. + +- `CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS`: Flag to indicate that the decoding should fail if the input contains + entry that collide on property keys. + +- `CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES`: Flag to indicate that the decoding should fail if the input contains + null values. + +- `CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS`: Flag to indicate that the decoding should fail if the input contains + empty arrays. + +- `CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS`: Flag to indicate that the decoding should fail if the input contains + empty keys. + +- `CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS`: Flag to indicate that the decoding should fail if the input contains + mixed arrays. + +- `CELIX_PROPERTIES_DECODE_STRICT`: Flag to indicate that the decoding should fail if the input contains any of the + decode error flags. From c46a483bc8031edc14e11739a8bec5aa7043e4ee Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 15 Apr 2024 23:25:05 +0200 Subject: [PATCH 16/25] gh-685: Some whitespace corrections --- conanfile.py | 2 +- libs/error_injector/jansson/src/jansson_ei.cc | 2 +- .../error_injector/celix_version/src/celix_version_ei.cc | 2 +- libs/utils/gtest/src/PropertiesEncodingTestSuite.cc | 2 +- libs/utils/include/celix_utils.h | 9 ++++----- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/conanfile.py b/conanfile.py index d8b46d017..dbf956bad 100644 --- a/conanfile.py +++ b/conanfile.py @@ -309,7 +309,7 @@ def configure(self): self.options['openssl'].shared = True if self.options.build_celix_dfi: self.options['libffi'].shared = True - if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib: + if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib: self.options['jansson'].shared = True def requirements(self): diff --git a/libs/error_injector/jansson/src/jansson_ei.cc b/libs/error_injector/jansson/src/jansson_ei.cc index 1d820629e..515d45674 100644 --- a/libs/error_injector/jansson/src/jansson_ei.cc +++ b/libs/error_injector/jansson/src/jansson_ei.cc @@ -106,4 +106,4 @@ json_t* __wrap_json_sprintf(const char* fmt, ...) { return obj; } -} \ No newline at end of file +} diff --git a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc index ddcdf9f81..b09339cda 100644 --- a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc +++ b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc @@ -48,4 +48,4 @@ char* __wrap_celix_version_toString(const celix_version_t* version) { return __real_celix_version_toString(version); } -} \ No newline at end of file +} diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 3e03b77ef..4232131bb 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -914,4 +914,4 @@ TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) { // And the loaded properties are equal to the original properties EXPECT_TRUE(celix_properties_equals(props, loadedProps)); -} \ No newline at end of file +} diff --git a/libs/utils/include/celix_utils.h b/libs/utils/include/celix_utils.h index 6f58f2b76..19a4fd94e 100644 --- a/libs/utils/include/celix_utils.h +++ b/libs/utils/include/celix_utils.h @@ -97,9 +97,7 @@ CELIX_UTILS_EXPORT void celix_utils_freeStringIfNotEqual(const char* buffer, cha * @brief Guard for a string created with celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString. * * Can be used with celix_auto() to automatically and correctly free the string. - * If the string is pointing to the buffer, the string should be freed, otherwise the string should be freed. - * - * + * If the string is pointing to the buffer, the string should not be freed, otherwise the string should be freed. */ typedef struct celix_utils_string_guard { const char* buffer; @@ -107,7 +105,8 @@ typedef struct celix_utils_string_guard { } celix_utils_string_guard_t; /** - * @brief Initialize a guard for a string created with celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString. + * @brief Initialize a guard for a string created with celix_utils_writeOrCreateString or + * celix_utils_writeOrCreateVString. * * De-initialize with celix_utils_stringGuard_deinit(). * @@ -115,7 +114,7 @@ typedef struct celix_utils_string_guard { * This is intended to be used with celix_auto(). * * * Example: -* ``` + * ``` * const char* possibleLongString = ... * char buffer[64]; * char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "Hello %s", possibleLongString); From 19a22169c0081766ff409a8e9734bcd90ced6f34 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Tue, 16 Apr 2024 20:54:32 +0200 Subject: [PATCH 17/25] gh-685: Fix celix_properties_setVersion ei test --- libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc index 162a86fcd..e7916ea38 100644 --- a/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc @@ -46,6 +46,7 @@ class PropertiesErrorInjectionTestSuite : public ::testing::Test { celix_ei_expect_open_memstream(nullptr, 0, nullptr); celix_ei_expect_asprintf(nullptr, 0, -1); celix_ei_expect_malloc(nullptr, 0, nullptr); + celix_ei_expect_calloc(nullptr, 0, nullptr); celix_ei_expect_celix_stringHashMap_createWithOptions(nullptr, 0, nullptr); celix_ei_expect_celix_arrayList_copy(nullptr, 0, nullptr); celix_ei_expect_celix_utils_strdup(nullptr, 0, nullptr); @@ -453,11 +454,12 @@ TEST_F(PropertiesErrorInjectionTestSuite, SetVersionFailureTest) { celix_err_resetErrors(); celix_autoptr(celix_version_t) version2 = celix_version_create(1, 2, 3, "aaaaaaaaaaaaaaaaaaaaaaaaaa"); - celix_ei_expect_asprintf((void*) celix_version_toString, 0, -1); + celix_ei_expect_calloc((void*) celix_version_create, 0, nullptr); status = celix_properties_setVersion(props, "key", version2); ASSERT_EQ(status, CELIX_ENOMEM); - ASSERT_STREQ("Cannot fill property entry", celix_err_popLastError()); - ASSERT_STREQ("Failed to allocate memory for celix_version_toString", celix_err_popLastError()); + EXPECT_EQ(2, celix_err_getErrorCount()); + ASSERT_STREQ("Failed to copy version", celix_err_popLastError()); + ASSERT_STREQ("Failed to allocate memory for celix_version_create", celix_err_popLastError()); celix_err_resetErrors(); fillOptimizationCache(props); From 7aa44860b9335bb5e3ec9d1642cc4e3fb2547b14 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Tue, 16 Apr 2024 23:48:23 +0200 Subject: [PATCH 18/25] gh-685: Add JSON C++ save/load wrapper methods --- ...opertiesEncodingErrorInjectionTestSuite.cc | 43 ++++- .../gtest/src/PropertiesEncodingTestSuite.cc | 85 +++++++++ libs/utils/include/celix/Properties.h | 179 +++++++++++++++++- libs/utils/include/celix_properties.h | 3 +- 4 files changed, 306 insertions(+), 4 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc index 2553e0e96..03edf18e0 100644 --- a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -19,9 +19,10 @@ #include -#include "celix_err.h" #include "celix_properties.h" +#include "celix/Properties.h" +#include "celix_err.h" #include "celix_array_list_ei.h" #include "celix_utils_ei.h" #include "celix_version_ei.h" @@ -303,3 +304,43 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeVersionErrorTest) { EXPECT_EQ(1, celix_err_getErrorCount()); celix_err_printErrors(stderr, "Test Error: ", "\n"); } + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveCxxPropertiesErrorTest) { + //Given a dummy Properties object + celix::Properties props{}; + props.set("key", "value"); + + //When an error injected is prepared for json_object() from saveToStream + celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr); + + //Then saving to file throws a bad alloc exception + EXPECT_THROW(props.save("somefile.json"), std::bad_alloc); + + //When an error injected is prepared for json_object() from saveToStream + celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr); + + //Then saving to string throws a bad alloc exception + EXPECT_THROW(props.saveToString(), std::bad_alloc); +} + +TEST_F(PropertiesEncodingErrorInjectionTestSuite, LoadCxxPropertiesErrorTest) { + //Given a dummy json string + const char* json = R"({"key":"value"})"; + + //When an error injected is prepared for malloc() from celix_properties_create + celix_ei_expect_malloc((void*)celix_properties_create, 0, nullptr); + + //Then loading from string throws a bad alloc exception + EXPECT_THROW(celix::Properties::loadFromString(json), std::bad_alloc); + + //When an error injected is prepared for malloc() from celix_properties_create + celix_ei_expect_malloc((void*)celix_properties_create, 0, nullptr); + + //And an empty json file exists + FILE* file = fopen("empty.json", "w"); + fprintf(file, "{}"); + fclose(file); + + //Then loading from file throws a bad alloc exception + EXPECT_THROW(celix::Properties::load2("empty.json"), std::bad_alloc); +} diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 4232131bb..688db8ef5 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -21,10 +21,12 @@ #include #include +#include "celix/Properties.h" #include "celix_err.h" #include "celix_properties.h" #include "celix_stdlib_cleanup.h" + class PropertiesSerializationTestSuite : public ::testing::Test { public: PropertiesSerializationTestSuite() { celix_err_resetErrors(); } @@ -418,6 +420,42 @@ TEST_F(PropertiesSerializationTestSuite, SaveWithInvalidStreamTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); } +TEST_F(PropertiesSerializationTestSuite, SaveCxxPropertiesTest) { + // Given a C++ Properties object with 2 keys + celix::Properties props{}; + props.set("key1", "value1"); + props.set("key2", 42); + props.setVector("key3", std::vector{}); // empty vector + + // When saving the properties to a string + std::string result = props.saveToString(); + + // Then the result contains the JSON representation snippets of the properties + EXPECT_NE(std::string::npos, result.find("\"key1\":\"value1\"")); + EXPECT_NE(std::string::npos, result.find("\"key2\":42")); + + // When saving the properties to a string using a flat style + std::string result2 = props.saveToString(celix::Properties::EncodingFlags::FlatStyle); + + //The result is equals to a default save + EXPECT_EQ(result, result2); + + // When saving the properties to a string using an errors on duplicate key flag + EXPECT_THROW(props.saveToString(celix::Properties::EncodingFlags::Strict), + celix::IOException); + + // When saving the properties to a string using combined flags + EXPECT_THROW(props.saveToString( + celix::Properties::EncodingFlags::Pretty | celix::Properties::EncodingFlags::ErrorOnEmptyArrays | + celix::Properties::EncodingFlags::ErrorOnCollisions | + celix::Properties::EncodingFlags::ErrorOnNanInf | celix::Properties::EncodingFlags::NestedStyle), + celix::IOException); + + // When saving the properties to an invalid filename location + EXPECT_THROW(props.save("/non-existing/no/rights/file.json"), + celix::IOException); +} + TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { //Given an empty JSON object const char* json = "{}"; @@ -863,6 +901,35 @@ TEST_F(PropertiesSerializationTestSuite, LoadWithInvalidStreamTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); } +TEST_F(PropertiesSerializationTestSuite, LoadCxxPropertiesTest) { + // Given a JSON object + auto jsonInput = R"({"key1":"value1","key2":42,"key2":43})"; // note duplicate key3 + + // When loading the properties from the JSON object + auto props = celix::Properties::loadFromString(jsonInput); + + // Then the properties object contains the values + EXPECT_EQ(2, props.size()); + EXPECT_STREQ("value1", props.get("key1").c_str()); + EXPECT_EQ(43, props.getAsLong("key2", -1)); + + // When loading the properties from the JSON object with a strict flag + EXPECT_THROW(celix::Properties::loadFromString(jsonInput, celix::Properties::DecodeFlags::Strict), + celix::IOException); + + // When loading the properties from the JSON object with a flag combined + EXPECT_THROW( + celix::Properties::loadFromString( + jsonInput, + celix::Properties::DecodeFlags::ErrorOnCollisions | celix::Properties::DecodeFlags::ErrorOnDuplicates | + celix::Properties::DecodeFlags::ErrorOnEmptyArrays | celix::Properties::DecodeFlags::ErrorOnEmptyKeys | + celix::Properties::DecodeFlags::ErrorOnMixedArrays | celix::Properties::DecodeFlags::ErrorOnNullValues | + celix::Properties::DecodeFlags::ErrorOnNullValues), + celix::IOException); + + EXPECT_THROW(celix::Properties::load2("non_existing_file.json"), celix::IOException); +} + TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) { // Given a properties object with all possible types (but no empty arrays) celix_autoptr(celix_properties_t) props = celix_properties_create(); @@ -915,3 +982,21 @@ TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) { // And the loaded properties are equal to the original properties EXPECT_TRUE(celix_properties_equals(props, loadedProps)); } + +TEST_F(PropertiesSerializationTestSuite, SaveAndLoadCxxProperties) { + //Given a filename + std::string filename = "properties_test.json"; + + //And a Properties object with 1 key + celix::Properties props{}; + props.set("key1", "value1"); + + //When saving the properties to the filename + props.save(filename); + + //And reloading the properties from the filename + auto props2 = celix::Properties::load2(filename); + + //Then the reloaded properties are equal to the original properties + EXPECT_TRUE(props == props2); +} \ No newline at end of file diff --git a/libs/utils/include/celix/Properties.h b/libs/utils/include/celix/Properties.h index 3b784c5fa..953715832 100644 --- a/libs/utils/include/celix/Properties.h +++ b/libs/utils/include/celix/Properties.h @@ -947,13 +947,162 @@ namespace celix { storeTo(path.data(), header.empty() ? nullptr : header.data()); } + /** + * @brief Enum class for encoding flags used in Celix properties JSON encoding. + * + * The flags are used to control the encoding process and to specify the output format. + * + * @enum EncodingFlags + */ + enum class EncodingFlags : int { + None = 0, /**< No special encoding flags. */ + Pretty = + CELIX_PROPERTIES_ENCODE_PRETTY, /**< Encode in a pretty format, with indentation and line breaks. */ + FlatStyle = + CELIX_PROPERTIES_ENCODE_FLAT_STYLE, /**< Encode in a flat style, with all keys at the top level. */ + NestedStyle = CELIX_PROPERTIES_ENCODE_NESTED_STYLE, /**< Encode in a nested style, with nested objects for + each key based on a `/` separator. */ + ErrorOnCollisions = CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, /**< If set, encoding will fail if there + are collisions between keys. */ + ErrorOnEmptyArrays = CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, /**< If set, encoding will fail if there + are empty arrays. */ + ErrorOnNanInf = CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, /**< If set, encoding will fail if there are NaN + or Inf values. */ + Strict = CELIX_PROPERTIES_ENCODE_STRICT, /**< If set, encoding will fail if there are any errors. */ + }; + + /** + * @brief Save (encode) this properties object as a JSON representation to a file. + * + * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream + * + * For a overview of the possible encode flags, see the EncodingFlags flags documentation. + * The default encoding style is a compact and flat JSON representation. + * + * @param[in] filename The file to write the JSON representation of the properties object to. + * @param[in] encodingFlags The flags to use when encoding the input string. + * @throws celix::IOException If an error occurs while writing to the file. + * @throws std::bad_alloc If there was not enough memory to save the properties. + */ + void save(const std::string& filename, EncodingFlags encodingFlags = EncodingFlags::None) const { + auto status = celix_properties_save(cProps.get(), filename.c_str(), static_cast(encodingFlags)); + if (status == ENOMEM) { + throw std::bad_alloc(); + } else if (status != CELIX_SUCCESS) { + throw celix::IOException{"Cannot save celix::Properties to " + filename}; + } + } + + /** + * @brief Save (encode) this properties object as a JSON representation to a string. + * + * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream + * + * For a overview of the possible encode flags, see the EncodingFlags flags documentation. + * The default encoding style is a compact and flat JSON representation. + * + * @param[in] encodeFlags The flags to use when encoding the input string. + * @throws celix::IOException If an error occurs while writing to the file. + * @throws std::bad_alloc If there was not enough memory to save the properties. + */ + std::string saveToString(EncodingFlags encodeFlags = EncodingFlags::None) const { + char* str = nullptr; + auto status = celix_properties_saveToString(cProps.get(), static_cast(encodeFlags), &str); + if (status == ENOMEM) { + throw std::bad_alloc(); + } else if (status != CELIX_SUCCESS) { + throw celix::IOException{"Cannot save celix::Properties to string"}; + } + std::string result{str}; + free(str); + return result; + } + + /** + * @brief Enum class for decoding flags used in Celix properties JSON decoding. + * + * The flags are used to control the decoding process and to specify the output format. + * + * @enum DecodeFlags + */ + enum class DecodeFlags : int { + None = 0, /**< No special decoding flags. */ + ErrorOnDuplicates = CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, /**< If set, decoding will fail if there + are duplicate keys. */ + ErrorOnCollisions = CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, /**< If set, decoding will fail if there + are collisions between keys. */ + ErrorOnNullValues = CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES, /**< If set, decoding will fail if there + are null values. */ + ErrorOnEmptyArrays = CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, /**< If set, decoding will fail if there + are empty arrays. */ + ErrorOnEmptyKeys = CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS, /**< If set, decoding will fail if there are + empty ("") keys. */ + ErrorOnMixedArrays = CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS, /**< If set, decoding will fail if there + are mixed type arrays. */ + Strict = CELIX_PROPERTIES_DECODE_STRICT /**< If set, decoding will fail if there are any errors. */ + }; + /** * @brief Loads properties from the file at the given path. * @param[in] path The path to the file containing the properties. * @return A new Properties object containing the properties from the file. * @throws celix::IOException If the file cannot be opened or read. */ - static celix::Properties load(const std::string& path) { return loadFrom(path.data()); } + static Properties load(const std::string& path) { return loadFrom(path.data()); } + + /** + * @brief Load a Properties object from a file. + * + * @warning The name if temporary and will be renamed to celix::Properties::load in the future (when + * the current celix::Properties::load is removed). + * + * The content of the filename file is expected to be in the format of a JSON object. + * For what can and cannot be parsed, see celix_properties_loadFromStream documentation. + * + * For a overview of the possible decode flags, see the DecodingFlags flags documentation. + * + * @param[in] filename The file to load the properties from. + * @param[in] decodeFlags The flags to use when decoding the input string. + * @return A new Properties object containing the properties from the file. + * @throws celix::IOException If the file cannot be opened or read. + * @throws std::bad_alloc If there was not enough memory to load the properties. + */ + static Properties load2(const std::string& filename, DecodeFlags decodeFlags = DecodeFlags::None) { + celix_properties_t* props; + auto status = celix_properties_load2(filename.c_str(), static_cast(decodeFlags), &props); + if (status == ENOMEM) { + throw std::bad_alloc(); + } else if (status != CELIX_SUCCESS) { + throw celix::IOException{"Cannot load celix::Properties from " + filename}; + } + return celix::Properties::own(props); + } + + /** + * @brief Load a Properties object from a string. + * + * + * The input string is expected to be in the format of a JSON object. + * For what can and cannot be parsed, see celix_properties_loadFromStream documentation. + * + * For a overview of the possible decode flags, see the DecodingFlags flags documentation. + * + * @param[in] input The input string to parse. + * @param[in] decodeFlags The flags to use when decoding the input string. + * @return A new Properties object containing the properties from the file. + * @throws celix::IOException If the file cannot be opened or read. + * @throws std::bad_alloc If there was not enough memory to load the properties. + */ + static Properties loadFromString(const std::string& input, DecodeFlags decodeFlags = DecodeFlags::None) { + celix_properties_t* props; + auto status = celix_properties_loadFromString2(input.c_str(), static_cast(decodeFlags), &props); + if (status == ENOMEM) { + throw std::bad_alloc(); + } else if (status != CELIX_SUCCESS) { + throw celix::IOException{"Cannot load celix::Properties from string"}; + } + return celix::Properties::own(props); + } private: Properties(celix_properties_t* props, bool takeOwnership) : @@ -1045,8 +1194,36 @@ namespace celix { }; } +/** + * @brief Stream operator to print the properties value reference to a stream. + * @param[in] os The stream to print the properties to. + * @param[in] ref The properties value reference to print. + * @return The os stream. + */ inline std::ostream& operator<<(std::ostream& os, const ::celix::Properties::ValueRef& ref) { os << std::string{ref.getValue()}; return os; } + +/** + * @brief Bitwise OR operator for EncodingFlags. + * @param[in] a encoding flags + * @param[in] b encoding flags + * @return The bitwise OR of the two encoding flags. + */ +inline ::celix::Properties::EncodingFlags operator|(::celix::Properties::EncodingFlags a, + ::celix::Properties::EncodingFlags b) { + return static_cast<::celix::Properties::EncodingFlags>(static_cast(a) | static_cast(b)); +} + +/** + * @brief Bitwise OR operator for DecodeFlags. + * @param[in] a decoding flags + * @param[in] b decoding flags + * @return The bitwise OR of the two decoding flags. + */ +inline ::celix::Properties::DecodeFlags operator|(::celix::Properties::DecodeFlags a, + ::celix::Properties::DecodeFlags b) { + return static_cast<::celix::Properties::DecodeFlags>(static_cast(a) | static_cast(b)); +} diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index 3336392e0..ee9b780c9 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -1085,7 +1085,6 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_prop * @param[in] properties The properties object to encode. * @param[in] filename The file to write the JSON representation of the properties object to. * @param[in] encodeFlags The flags to use when encoding the input string. - * returned string using free. * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be * encoded to a JSON representation and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file * could not be opened or written to. @@ -1233,7 +1232,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, /** * @brief Load properties from a file. * - * @warning The name if temporary and will be renamed to celix_properties_load2 in the future (when + * @warning The name if temporary and will be renamed to celix_properties_load in the future (when * the current celix_properties_load is removed). * * The content of the filename file is expected to be in the format of a JSON object. From 3a761223211941bcda914477fd5cf5d7bd1097dc Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 20 May 2024 15:55:12 +0200 Subject: [PATCH 19/25] gh-685: Fix memleak and add missing test --- libs/error_injector/jansson/CMakeLists.txt | 1 + .../jansson/include/jansson_ei.h | 1 + libs/error_injector/jansson/src/jansson_ei.cc | 7 +++++ ...opertiesEncodingErrorInjectionTestSuite.cc | 20 +++++++++++++ .../gtest/src/PropertiesEncodingTestSuite.cc | 16 ++++++++++ libs/utils/gtest/src/PropertiesTestSuite.cc | 1 + libs/utils/include/celix_properties.h | 13 +++++---- libs/utils/src/properties_encoding.c | 29 ++++++++++++------- 8 files changed, 72 insertions(+), 16 deletions(-) diff --git a/libs/error_injector/jansson/CMakeLists.txt b/libs/error_injector/jansson/CMakeLists.txt index b093901d6..ce4beaa9d 100644 --- a/libs/error_injector/jansson/CMakeLists.txt +++ b/libs/error_injector/jansson/CMakeLists.txt @@ -37,5 +37,6 @@ target_link_options(jansson_ei INTERFACE LINKER:--wrap,json_real LINKER:--wrap,json_vsprintf LINKER:--wrap,json_sprintf + LINKER:--wrap,json_dumpf ) add_library(Celix::jansson_ei ALIAS jansson_ei) diff --git a/libs/error_injector/jansson/include/jansson_ei.h b/libs/error_injector/jansson/include/jansson_ei.h index 167ace857..b98bd8821 100644 --- a/libs/error_injector/jansson/include/jansson_ei.h +++ b/libs/error_injector/jansson/include/jansson_ei.h @@ -36,6 +36,7 @@ CELIX_EI_DECLARE(json_string, json_t*); CELIX_EI_DECLARE(json_real, json_t*); CELIX_EI_DECLARE(json_vsprintf,json_t*); CELIX_EI_DECLARE(json_sprintf, json_t*); +CELIX_EI_DECLARE(json_dumpf, int); #ifdef __cplusplus } diff --git a/libs/error_injector/jansson/src/jansson_ei.cc b/libs/error_injector/jansson/src/jansson_ei.cc index 515d45674..98289c970 100644 --- a/libs/error_injector/jansson/src/jansson_ei.cc +++ b/libs/error_injector/jansson/src/jansson_ei.cc @@ -106,4 +106,11 @@ json_t* __wrap_json_sprintf(const char* fmt, ...) { return obj; } +int __real_json_dumpf(const json_t* json, FILE* output, size_t flags); +CELIX_EI_DEFINE(json_dumpf, int) +int __wrap_json_dumpf(const json_t* json, FILE* output, size_t flags) { + CELIX_EI_IMPL(json_dumpf); + return __real_json_dumpf(json, output, flags); } + +} \ No newline at end of file diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc index 03edf18e0..7e5276c47 100644 --- a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -210,6 +210,26 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeVersionErrorTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); } +TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeDumpfErrorTest) { + // Given a dummy properties object + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key", "value"); + + // When an error injected is prepared for json_dumpf() from celix_properties_saveToString + celix_ei_expect_json_dumpf((void*)celix_properties_saveToStream, 0, -1); + + // And I call celix_properties_saveToString + char* out; + auto status = celix_properties_saveToString(props, 0, &out); + + // Then I expect an error + EXPECT_EQ(ENOMEM, status); + + // And I expect 1 error message in celix_err + EXPECT_EQ(1, celix_err_getErrorCount()); + celix_err_printErrors(stderr, "Test Error: ", "\n"); +} + TEST_F(PropertiesEncodingErrorInjectionTestSuite, LoadErrorTest) { //Given a dummy json string const char* json = R"({"key":"value"})"; diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 688db8ef5..1c92d2f80 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -259,7 +259,23 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { json_decref(root); } +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNestedEndErrorOnCollisionsFlagsTest) { + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1", "value1"); + celix_properties_set(props, "key2", "value2"); + celix_properties_set(props, "object1/key3", "value3"); + celix_properties_set(props, "object1/key4", "value4"); + celix_properties_set(props, "object2/key5", "value5"); + celix_properties_set(props, "object3/object4/key6", "value6"); + // And an in-memory stream + celix_autofree char* output; + + // When saving the properties to the stream + auto status = celix_properties_saveToString( + props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output); + ASSERT_EQ(CELIX_SUCCESS, status); +} TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTest) { //Given a properties set with key names with slashes diff --git a/libs/utils/gtest/src/PropertiesTestSuite.cc b/libs/utils/gtest/src/PropertiesTestSuite.cc index 4658a8666..f44e0b077 100644 --- a/libs/utils/gtest/src/PropertiesTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesTestSuite.cc @@ -24,6 +24,7 @@ #include "celix_err.h" #include "celix_properties.h" #include "celix_properties_internal.h" +#include "celix_stdlib_cleanup.h" #include "celix_utils.h" using ::testing::MatchesRegex; diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index ee9b780c9..aa9f962d3 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -999,7 +999,8 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i * celix_properties_setString(properties, "key/with/slash", "value1"); * celix_properties_setString(properties, "key", "value2"); //collision * char* json; - * celix_status_t status = celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_NESTED, &json); + * celix_status_t status = celix_properties_saveToString(properties, + * CELIX_PROPERTIES_ENCODE_NESTED | | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &json); * // status will be CELIX_ILLEGAL_ARGUMENT and a error message will be logged to celix_err * @endcode * @@ -1033,7 +1034,8 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i * @brief Flag to indicate that all encode "error on" flags should be set. */ #define CELIX_PROPERTIES_ENCODE_STRICT \ - (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) + (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS | \ + CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) /** * @brief Save (encode) as a JSON representation to a stream. @@ -1042,7 +1044,7 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i * * Properties are encoded as a JSON object. * - * If no encoding style flag is set of when the CELIX_PROPERTIES_ENCODE_FLAT_STYLE flag is set, properties + * If no encoding style flag is set or when the CELIX_PROPERTIES_ENCODE_FLAT_STYLE flag is set, properties * entries are written as top level field entries. * * If the CELIX_PROPERTIES_ENCODE_NESTED_STYLE flag is set, properties entry keys are split on '/' and nested in @@ -1066,9 +1068,10 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i * * @param properties The properties object to encode. * @param stream The stream to write the JSON representation of the properties object to. - * @param encodeFlags The flags to use when encoding the input string. + * @param encodeFlags The flags to use when encoding the input properties. * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be - * encoded to a JSON representation and ENOMEM if there was not enough memory. + * encoded to a JSON representation, ENOMEM if there was not enough memory and CELIX_FILE_IO_EXCEPTION if the stream + * could not be written to. */ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index 74103f484..ff3bd9783 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -28,6 +28,8 @@ #include #include +#define CELIX_PROPERTIES_JPATH_SEPARATOR '/' + static celix_status_t celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags); @@ -169,6 +171,7 @@ static celix_status_t celix_properties_addJsonValueToJson(json_t* value, const c if (field) { if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { celix_err_pushf("Invalid key collision. key '%s' already exists.", key); + json_decref(value); return CELIX_ILLEGAL_ARGUMENT; } } @@ -197,7 +200,7 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop int flags) { json_t* jsonObj = root; const char* fieldName = key; - const char* slash = strstr(key, "/"); + const char* slash = strchr(key, CELIX_PROPERTIES_JPATH_SEPARATOR); while (slash) { char buf[64]; char* name = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)(slash - fieldName), fieldName); @@ -207,11 +210,11 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop return ENOMEM; } json_t* subObj = json_object_get(jsonObj, name); - if (!subObj || !json_is_object(subObj)) { - if (!json_is_object(subObj) && flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { - celix_err_pushf("Invalid key collision. Key '%s' already exists.", name); - return CELIX_ILLEGAL_ARGUMENT; - } + if (subObj && !json_is_object(subObj) && flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { + celix_err_pushf("Invalid key collision. Key '%s' already exists.", name); + return CELIX_ILLEGAL_ARGUMENT; + } + if (!subObj) { subObj = json_object(); if (!subObj) { celix_err_push("Failed to create json object"); @@ -226,7 +229,7 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop jsonObj = subObj; fieldName = slash + 1; - slash = strstr(fieldName, "/"); + slash = strchr(fieldName, CELIX_PROPERTIES_JPATH_SEPARATOR); } json_t* value; @@ -297,11 +300,15 @@ celix_status_t celix_properties_saveToString(const celix_properties_t* propertie } celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); - fclose(stream); - if (status == CELIX_SUCCESS) { - *out = celix_steal_ptr(buffer); + (void)fclose(stream); + if (!buffer || status != CELIX_SUCCESS) { + if (!buffer || status == CELIX_FILE_IO_EXCEPTION) { + return ENOMEM; // Using memstream as stream, return ENOMEM instead of CELIX_FILE_IO_EXCEPTION + } + return status; } - return status; + *out = celix_steal_ptr(buffer); + return CELIX_SUCCESS; } static celix_status_t celix_properties_parseVersion(const char* value, celix_version_t** out) { From dbe1468d3945605f085a23f470e29ce3788230a8 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 20 May 2024 19:58:13 +0200 Subject: [PATCH 20/25] gh-685: Add decode of unsupported JSON obj/array arrays --- ...opertiesEncodingErrorInjectionTestSuite.cc | 21 +++++- .../gtest/src/PropertiesEncodingTestSuite.cc | 71 +++++++++++++++++-- libs/utils/include/celix/Properties.h | 5 +- libs/utils/include/celix_properties.h | 34 +++++---- libs/utils/src/properties_encoding.c | 13 ++-- 5 files changed, 118 insertions(+), 26 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc index 7e5276c47..acc0662a9 100644 --- a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -44,7 +44,7 @@ class PropertiesEncodingErrorInjectionTestSuite : public ::testing::Test { celix_ei_expect_malloc(nullptr, 0, nullptr); celix_ei_expect_celix_arrayList_createWithOptions(nullptr, 0, nullptr); celix_ei_expect_celix_arrayList_addString(nullptr, 0, CELIX_SUCCESS); - + celix_err_resetErrors(); } }; @@ -81,6 +81,25 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveErrorTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); } +TEST_F(PropertiesEncodingErrorInjectionTestSuite, FcloseErrorWhenSaveTest) { + //Given a dummy properties object + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key", "value"); + + //When an error injected is prepared for fclose() from save + celix_ei_expect_fclose((void*)celix_properties_save, 0, -1); + + //And I call celix_properties_save + auto status = celix_properties_save(props, "somefile.json", 0); + + //Then I expect an error + EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status); + + //And I expect 1 error message in celix_err + EXPECT_EQ(1, celix_err_getErrorCount()); +} + + TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { // Given a dummy properties object celix_autoptr(celix_properties_t) props = celix_properties_create(); diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 1c92d2f80..49e00f4d5 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -201,6 +201,21 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); } +TEST_F(PropertiesSerializationTestSuite, SaveEmptyKeyTest) { + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_setString(props, "", "value"); + + celix_autofree char* output1; + auto status = celix_properties_saveToString(props, 0, &output1); + ASSERT_EQ(CELIX_SUCCESS, status); + + celix_autoptr(celix_properties_t) prop2 = nullptr; + status = celix_properties_loadFromString2(output1, 0, &prop2); + ASSERT_EQ(CELIX_SUCCESS, status); + + ASSERT_TRUE(celix_properties_equals(props, prop2)); +} + TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { //Given a properties object with jpath keys celix_autoptr(celix_properties_t) props = celix_properties_create(); @@ -791,8 +806,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagT R"({"key1":null})", // Null value gives error on strict R"({"":"value"})", // "" key gives error on strict R"({"emptyArr":[]})", // Empty array gives error on strict - R"({"key1":"val1", "key1":"val2"})",// Duplicate key gives error on strict - R"({"nullArr":[null,null]})", // Array with null values gives error on strict + R"({"key1":"val1", "key1":"val2"})"// Duplicate key gives error on strict }; for (auto& invalidInput: invalidInputs) { @@ -833,6 +847,55 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagT } } +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithUnsupportedArrayTypesTest) { + auto invalidArrays = { + R"({"objArray":[{"obj1": true}, {"obj2": true}]})", // Array with objects not supported. + R"({"arrayArray":[[1,2], [2,4]]})", // Array with array not supported. + R"({"nullArr":[null,null]})" // Array with null values gives error on strict + }; + + // Decode with no strict flag, will ignore the unsupported arrays + for (auto& invalidArray : invalidArrays) { + // When loading the properties from the string + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromString2(invalidArray, 0, &props); + + // Then decoding succeeds, because strict is disabled + ASSERT_EQ(CELIX_SUCCESS, status); + EXPECT_GE(celix_err_getErrorCount(), 0); + + // But the properties size is 0, because the all invalid inputs are ignored + EXPECT_EQ(0, celix_properties_size(props)); + } + + // Decode with strict flag, will fail on unsupported arrays + for (auto& invalidArray : invalidArrays) { + // When loading the properties from the string + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromString2(invalidArray, CELIX_PROPERTIES_DECODE_STRICT, &props); + + // Then decoding fails + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_resetErrors(); + + // When loading the properties from the CELIX_PROPETIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag + celix_properties_t* props2; + status = celix_properties_loadFromString2( + invalidArray, CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS, &props2); + + // Then decoding fails + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + //celix_err_resetErrors(); + celix_err_printErrors(stderr, "Test Error: ", "\n"); + } +} + TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSlashesInTheKeysTest) { // Given a complex JSON object const char* jsonInput = R"({ @@ -939,8 +1002,8 @@ TEST_F(PropertiesSerializationTestSuite, LoadCxxPropertiesTest) { jsonInput, celix::Properties::DecodeFlags::ErrorOnCollisions | celix::Properties::DecodeFlags::ErrorOnDuplicates | celix::Properties::DecodeFlags::ErrorOnEmptyArrays | celix::Properties::DecodeFlags::ErrorOnEmptyKeys | - celix::Properties::DecodeFlags::ErrorOnMixedArrays | celix::Properties::DecodeFlags::ErrorOnNullValues | - celix::Properties::DecodeFlags::ErrorOnNullValues), + celix::Properties::DecodeFlags::ErrorOnUnsupportedArrays | + celix::Properties::DecodeFlags::ErrorOnNullValues | celix::Properties::DecodeFlags::ErrorOnNullValues), celix::IOException); EXPECT_THROW(celix::Properties::load2("non_existing_file.json"), celix::IOException); diff --git a/libs/utils/include/celix/Properties.h b/libs/utils/include/celix/Properties.h index 953715832..166a59bb0 100644 --- a/libs/utils/include/celix/Properties.h +++ b/libs/utils/include/celix/Properties.h @@ -1037,8 +1037,9 @@ namespace celix { are empty arrays. */ ErrorOnEmptyKeys = CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS, /**< If set, decoding will fail if there are empty ("") keys. */ - ErrorOnMixedArrays = CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS, /**< If set, decoding will fail if there - are mixed type arrays. */ + ErrorOnUnsupportedArrays = + CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS, /**< If set, decoding will fail if there + are unsupported array types or mixed array types. */ Strict = CELIX_PROPERTIES_DECODE_STRICT /**< If set, decoding will fail if there are any errors. */ }; diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index aa9f962d3..5ceec16b4 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -1140,6 +1140,8 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_prop * * E.g. `{"key": null}` is a null value. * + * Note arrays with null values are handled by the CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag. + * * If this flag is set, the decoding will fail if the input contains a null value and if this flag is not set, the * decoding will not fail and the JSON null entry will be ignored. */ @@ -1159,28 +1161,32 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_prop #define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x08 /** - * @brief Flag to indicate that the decoding should fail if the input contains empty keys. - * - * E.g. `{"": "value"}` is an empty key. + * @brief Flag to indicate that the decoding should fail if the input contains unsupported arrays. * - * Note that empty keys are valid in JSON and valid in properties, but not always desired. + * Unsupported arrays are arrays that contain JSON objects, multiple arrays, arrays with null values and + * mixed arrays. + * E.g. + * - `{"key": [{"nested": "value"}]}` (array with JSON object) + * - `{"key": [[1,2],[3,4]]}` (array with array) + * - `{"key": [null,null]}` (array with null values) + * - `{"key": ["value", 1]}` (mixed array) * - * If this flag is set, the decoding will fail if the input contains an empty key and if this flag is not set, the - * decoding will not fail and the JSON empty key entry will be ignored. + * If this flag is set, the decoding will fail if the input contains an unsupported array and if this flag is not set, + * the decoding will not fail and the unsupported array entries will be ignored. */ -#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x10 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS 0x10 /** - * @brief Flag to indicate that the decoding should fail if the input contains mixed arrays. + * @brief Flag to indicate that the decoding should fail if the input contains empty keys. * - * E.g. `{"key": ["value", 1]}` is a mixed array. + * E.g. `{"": "value"}` is an empty key. * - * Note that mixed arrays are valid in JSON, but not cannot be decoded to a valid properties array entry. + * Note that empty keys are valid in JSON and valid in properties, but not always desired. * - * If this flag is set, the decoding will fail if the input contains a mixed array and if this flag is not set, the - * decoding will not fail and the JSON mixed array entry will be ignored. + * If this flag is set, the decoding will fail if the input contains an empty key and if this flag is not set, the + * decoding will not fail and the JSON empty key entry will be ignored. */ -#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x20 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x20 /** * @brief Flag to indicate that the decoding should fail if the input contains any of the decode error flags. @@ -1190,7 +1196,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_prop #define CELIX_PROPERTIES_DECODE_STRICT \ (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS | \ CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | \ - CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) + CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS) /** * @brief Load properties from a stream. diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index ff3bd9783..a0ad9e437 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -285,7 +285,11 @@ celix_status_t celix_properties_save(const celix_properties_t* properties, const return CELIX_FILE_IO_EXCEPTION; } celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); - fclose(stream); + int rc = fclose(stream); + if (rc != 0) { + celix_err_pushf("Failed to close file %s: %s", filename, strerror(errno)); + return CELIX_FILE_IO_EXCEPTION; + } return status; } @@ -391,8 +395,8 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra case JSON_FALSE: *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL; break; - case JSON_NULL: default: + //JSON_NULL, JSON_OBJECT and JSON_ARRAY return CELIX_ILLEGAL_ARGUMENT; } @@ -403,9 +407,9 @@ static celix_status_t celix_properties_decodeArray(celix_properties_t* props, const char* key, const json_t* jsonArray, int flags) { celix_array_list_element_type_t elType; celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType); - if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)) { + if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS)) { celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY); - celix_err_pushf("Invalid mixed or null array for key '%s': %s.", key, arrStr); + celix_err_pushf("Invalid mixed, null, object or multidimensional array for key '%s': %s.", key, arrStr); return status; } else if (status != CELIX_SUCCESS) { //ignore mixed types @@ -461,7 +465,6 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* celix_err_push("Key cannot be empty."); return CELIX_ILLEGAL_ARGUMENT; } - return CELIX_SUCCESS; // ignore empty keys. } if (!json_is_object(jsonValue) && celix_properties_hasKey(props, key) && From 0b0edbe4328848f67f0e3e31b7c47bb825960287 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 20 May 2024 22:05:30 +0200 Subject: [PATCH 21/25] gh-685: Replace jsonpath seperator from / to . --- ...opertiesEncodingErrorInjectionTestSuite.cc | 2 +- .../gtest/src/PropertiesEncodingTestSuite.cc | 112 +++++++++--------- libs/utils/src/properties_encoding.c | 8 +- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc index acc0662a9..28590e939 100644 --- a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -103,7 +103,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, FcloseErrorWhenSaveTest) { TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) { // Given a dummy properties object celix_autoptr(celix_properties_t) props = celix_properties_create(); - celix_properties_set(props, "key/with/slash", "value"); + celix_properties_set(props, "key.with.slash", "value"); celix_properties_set(props, "key-with-out-slash", "value"); // When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_saveToString diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 49e00f4d5..3284b2f4a 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -216,15 +216,15 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyKeyTest) { ASSERT_TRUE(celix_properties_equals(props, prop2)); } -TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { +TEST_F(PropertiesSerializationTestSuite, SaveJSONPathKeysTest) { //Given a properties object with jpath keys celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); celix_properties_set(props, "key2", "value2"); - celix_properties_set(props, "object1/key3", "value3"); - celix_properties_set(props, "object1/key4", "value4"); - celix_properties_set(props, "object2/key5", "value5"); - celix_properties_set(props, "object3/object4/key6", "value6"); + celix_properties_set(props, "object1.key3", "value3"); + celix_properties_set(props, "object1.key4", "value4"); + celix_properties_set(props, "object2.key5", "value5"); + celix_properties_set(props, "object3.object4.key6", "value6"); //And an in-memory stream celix_autofree char* output; @@ -253,10 +253,10 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { //Given a properties object with jpath keys that collide celix_autoptr(celix_properties_t) props = celix_properties_create(); - celix_properties_set(props, "key1/key2/key3", "value1"); - celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2/key3" -> overwrite - celix_properties_set(props, "key4/key5/key6/key7", "value4"); - celix_properties_set(props, "key4/key5/key6", "value3"); //collision with field "key4/key5/key6/key7" -> overwrite + celix_properties_set(props, "key1.key2.key3", "value1"); + celix_properties_set(props, "key1.key2", "value2"); //collision with object "key1/key2/key3" -> overwrite + celix_properties_set(props, "key4.key5.key6.key7", "value4"); + celix_properties_set(props, "key4.key5.key6", "value3"); //collision with field "key4/key5/key6/key7" -> overwrite //When saving the properties to a string celix_autofree char* output = nullptr; @@ -278,10 +278,10 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNestedEndErrorOnColli celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); celix_properties_set(props, "key2", "value2"); - celix_properties_set(props, "object1/key3", "value3"); - celix_properties_set(props, "object1/key4", "value4"); - celix_properties_set(props, "object2/key5", "value5"); - celix_properties_set(props, "object3/object4/key6", "value6"); + celix_properties_set(props, "object1.key3", "value3"); + celix_properties_set(props, "object1.key4", "value4"); + celix_properties_set(props, "object2.key5", "value5"); + celix_properties_set(props, "object3.object4.key6", "value6"); // And an in-memory stream celix_autofree char* output; @@ -292,17 +292,17 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNestedEndErrorOnColli ASSERT_EQ(CELIX_SUCCESS, status); } -TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTest) { - //Given a properties set with key names with slashes +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithDotsTest) { + //Given a properties set with key names with dots celix_autoptr(celix_properties_t) props = celix_properties_create(); - celix_properties_set(props, "a/key/name/with/slashes", "value1"); - celix_properties_set(props, "/keyThatStartsWithSlash", "value3"); - celix_properties_set(props, "keyThatEndsWithSlash/", "value5"); - celix_properties_set(props, "keyThatEndsWithDoubleSlashes//", "value6"); - celix_properties_set(props, "key//With//Double//Slashes", "value7"); - celix_properties_set(props, "object/keyThatEndsWithSlash/", "value8"); - celix_properties_set(props, "object/keyThatEndsWithDoubleSlashes//", "value9"); - celix_properties_set(props, "object/key//With//Double//Slashes", "value10"); + celix_properties_set(props, "a.key.name.with.dots", "value1"); + celix_properties_set(props, ".keyThatStartsWithDot", "value3"); + celix_properties_set(props, "keyThatEndsWithDot.", "value5"); + celix_properties_set(props, "keyThatEndsWithDoubleDots..", "value6"); + celix_properties_set(props, "key..With..Double..Dots", "value7"); + celix_properties_set(props, "object.keyThatEndsWithDot.", "value8"); + celix_properties_set(props, "object.keyThatEndsWithDoubleDots..", "value9"); + celix_properties_set(props, "object.key..With..Double..Dots", "value10"); //When saving the properties to a string @@ -311,14 +311,14 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTe ASSERT_EQ(CELIX_SUCCESS, status); //Then the out contains the JSON representation snippets of the properties - EXPECT_NE(nullptr, strstr(output, R"("a":{"key":{"name":{"with":{"slashes":"value1"}}}})")) << "JSON: " << output; - EXPECT_NE(nullptr, strstr(output, R"("keyThatStartsWithSlash":"value3")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("a":{"key":{"name":{"with":{"dots":"value1"}}}})")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("keyThatStartsWithDot":"value3")")) << "JSON: " << output; EXPECT_NE(nullptr, strstr(output, R"("":"value5")")) << "JSON: " << output; EXPECT_NE(nullptr, strstr(output, R"("":"value6")")) << "JSON: " << output; - EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value7")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("Dots":"value7")")) << "JSON: " << output; EXPECT_NE(nullptr, strstr(output, R"("":"value8")")) << "JSON: " << output; EXPECT_NE(nullptr, strstr(output, R"("":"value9")")) << "JSON: " << output; - EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value10")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("Dots":"value10")")) << "JSON: " << output; //And the output is a valid JSON object json_error_t error; @@ -348,7 +348,7 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTe node = json_object_get(node, ""); ASSERT_NE(nullptr, node); ASSERT_TRUE(json_is_object(node)); - node = json_object_get(node, "Slashes"); + node = json_object_get(node, "Dots"); ASSERT_NE(nullptr, node); ASSERT_TRUE(json_is_string(node)); EXPECT_STREQ("value10", json_string_value(node)); @@ -363,8 +363,8 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) { //Given a properties that contains keys that will collide with an existing JSON object celix_autoptr(celix_properties_t) props = celix_properties_create(); - celix_properties_set(props, "key1/key2/key3", "value1"); - celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2" -> overwrite + celix_properties_set(props, "key1.key2.key3", "value1"); + celix_properties_set(props, "key1.key2", "value2"); //collision with object "key1.key2" -> overwrite //When saving the properties to a string celix_autofree char* output1; @@ -700,10 +700,10 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithNestedObjectsTest) { EXPECT_EQ(6, celix_properties_size(props)); EXPECT_STREQ("value1", celix_properties_getString(props, "key1")); EXPECT_STREQ("value2", celix_properties_getString(props, "key2")); - EXPECT_STREQ("value3", celix_properties_getString(props, "object1/key3")); - EXPECT_EQ(true, celix_properties_getBool(props, "object1/key4", false)); - EXPECT_DOUBLE_EQ(5., celix_properties_getDouble(props, "object2/key5", 0.0)); - EXPECT_EQ(6, celix_properties_getLong(props, "object3/object4/key6", 0)); + EXPECT_STREQ("value3", celix_properties_getString(props, "object1.key3")); + EXPECT_EQ(true, celix_properties_getBool(props, "object1.key4", false)); + EXPECT_DOUBLE_EQ(5., celix_properties_getDouble(props, "object2.key5", 0.0)); + EXPECT_EQ(6, celix_properties_getLong(props, "object3.object4.key6", 0)); } TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) { @@ -736,7 +736,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) { celix_err_printErrors(stderr, "Test Error: ", "\n"); } -TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedDotsTest) { // Given a complex JSON object with collisions and duplicate keys // Collisions: // - object object1/object2 and value object1/object2 @@ -749,12 +749,12 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { "object2": { "key1": "value1" }, - "object2/key2": "value2" + "object2.key2": "value2" }, - "object1/object2" : "value3", + "object1.object2" : "value3", "key3": "value4", "key3": "value5", - "object3/key4": "value6", + "object3.key4": "value6", "object3": { "key4": "value7" } @@ -770,11 +770,11 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { // And the properties object all the values for the colliding keys and a single (latest) value for the duplicate // keys EXPECT_EQ(5, celix_properties_size(props)); - EXPECT_STREQ("value1", celix_properties_getString(props, "object1/object2/key1")); - EXPECT_STREQ("value2", celix_properties_getString(props, "object1/object2/key2")); - EXPECT_STREQ("value3", celix_properties_getString(props, "object1/object2")); + EXPECT_STREQ("value1", celix_properties_getString(props, "object1.object2.key1")); + EXPECT_STREQ("value2", celix_properties_getString(props, "object1.object2.key2")); + EXPECT_STREQ("value3", celix_properties_getString(props, "object1.object2")); EXPECT_STREQ("value5", celix_properties_getString(props, "key3")); - EXPECT_STREQ("value7", celix_properties_getString(props, "object3/key4")); + EXPECT_STREQ("value7", celix_properties_getString(props, "object3.key4")); // When decoding the properties from a string using a flag that allows duplicates celix_properties_t* props2; @@ -881,7 +881,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithUnsupportedArrayTypes EXPECT_GE(celix_err_getErrorCount(), 1); celix_err_resetErrors(); - // When loading the properties from the CELIX_PROPETIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag + // When loading the properties from the CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag celix_properties_t* props2; status = celix_properties_loadFromString2( invalidArray, CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS, &props2); @@ -896,16 +896,16 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithUnsupportedArrayTypes } } -TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSlashesInTheKeysTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDotsInTheKeysTest) { // Given a complex JSON object const char* jsonInput = R"({ - "/": "value1", - "keyThatEndsWithSlash/": "value2", - "key//With//Double//Slash": "value3", + ".": "value1", + "keyThatEndsWithDots.": "value2", + "key..With..Double..Dots": "value3", "object": { - "/": "value4", - "keyThatEndsWithSlash/": "value5", - "key//With//Double//Slash": "value6" + ".": "value4", + "keyThatEndsWithDot.": "value5", + "key..With..Double..Dot": "value6" } })"; @@ -920,12 +920,12 @@ TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSlashesInTheKeysTest) // Then the properties object contains the nested objects EXPECT_EQ(6, celix_properties_size(props)); - EXPECT_STREQ("value1", celix_properties_getString(props, "/")); - EXPECT_STREQ("value2", celix_properties_getString(props, "keyThatEndsWithSlash/")); - EXPECT_STREQ("value3", celix_properties_getString(props, "key//With//Double//Slash")); - EXPECT_STREQ("value4", celix_properties_getString(props, "object//")); - EXPECT_STREQ("value5", celix_properties_getString(props, "object/keyThatEndsWithSlash/")); - EXPECT_STREQ("value6", celix_properties_getString(props, "object/key//With//Double//Slash")); + EXPECT_STREQ("value1", celix_properties_getString(props, ".")); + EXPECT_STREQ("value2", celix_properties_getString(props, "keyThatEndsWithDots.")); + EXPECT_STREQ("value3", celix_properties_getString(props, "key..With..Double..Dots")); + EXPECT_STREQ("value4", celix_properties_getString(props, "object..")); + EXPECT_STREQ("value5", celix_properties_getString(props, "object.keyThatEndsWithDot.")); + EXPECT_STREQ("value6", celix_properties_getString(props, "object.key..With..Double..Dot")); } TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidVersionsTest) { diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index a0ad9e437..b95c548e3 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -28,7 +28,7 @@ #include #include -#define CELIX_PROPERTIES_JPATH_SEPARATOR '/' +#define CELIX_PROPERTIES_JSONPATH_SEPARATOR '.' static celix_status_t celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags); @@ -200,7 +200,7 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop int flags) { json_t* jsonObj = root; const char* fieldName = key; - const char* slash = strchr(key, CELIX_PROPERTIES_JPATH_SEPARATOR); + const char* slash = strchr(key, CELIX_PROPERTIES_JSONPATH_SEPARATOR); while (slash) { char buf[64]; char* name = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)(slash - fieldName), fieldName); @@ -229,7 +229,7 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop jsonObj = subObj; fieldName = slash + 1; - slash = strchr(fieldName, CELIX_PROPERTIES_JPATH_SEPARATOR); + slash = strchr(fieldName, CELIX_PROPERTIES_JSONPATH_SEPARATOR); } json_t* value; @@ -491,7 +491,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* json_t* fieldValue; json_object_foreach(jsonValue, fieldName, fieldValue) { char buf[64]; - char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s/%s", key, fieldName); + char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s%c%s", key, CELIX_PROPERTIES_JSONPATH_SEPARATOR, fieldName); celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, combinedKey); if (!combinedKey) { celix_err_push("Failed to create sub key."); From 769a9e55b32015e6fa8f128b047468f434168e32 Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Mon, 20 May 2024 22:24:41 +0200 Subject: [PATCH 22/25] gh-685: Improve jansson error handling in properties encode --- .../gtest/src/PropertiesEncodingTestSuite.cc | 25 +++++++++++++ libs/utils/src/celix_properties_private.h | 7 ++++ libs/utils/src/properties_encoding.c | 37 ++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 3284b2f4a..2ebdf52db 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -24,6 +24,7 @@ #include "celix/Properties.h" #include "celix_err.h" #include "celix_properties.h" +#include "celix_properties_private.h" #include "celix_stdlib_cleanup.h" @@ -1078,4 +1079,28 @@ TEST_F(PropertiesSerializationTestSuite, SaveAndLoadCxxProperties) { //Then the reloaded properties are equal to the original properties EXPECT_TRUE(props == props2); +} + +TEST_F(PropertiesSerializationTestSuite, JsonErrorToCelixStatusTest) { + EXPECT_EQ(CELIX_ILLEGAL_STATE, celix_properties_jsonErrorToStatus(json_error_unknown)); + + EXPECT_EQ(ENOMEM, celix_properties_jsonErrorToStatus(json_error_out_of_memory)); + EXPECT_EQ(ENOMEM, celix_properties_jsonErrorToStatus(json_error_stack_overflow)); + + EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, celix_properties_jsonErrorToStatus(json_error_cannot_open_file)); + + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_argument)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_argument)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_premature_end_of_input)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_end_of_input_expected)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_syntax)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_format)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_wrong_type)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_character)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_value)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_byte_in_key)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_duplicate_key)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_numeric_overflow)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_item_not_found)); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_index_out_of_range)); } \ No newline at end of file diff --git a/libs/utils/src/celix_properties_private.h b/libs/utils/src/celix_properties_private.h index 209b2ef0c..350a37337 100644 --- a/libs/utils/src/celix_properties_private.h +++ b/libs/utils/src/celix_properties_private.h @@ -26,11 +26,13 @@ #define CELIX_CELIX_PROPERTIES_PRIVATE_H #include "celix_properties.h" +#include "jansson.h" #ifdef __cplusplus extern "C" { #endif + /** * @brief Alloc new entry for the provided properties. Possible using the properties optimizer cache. */ @@ -41,6 +43,11 @@ celix_properties_entry_t* celix_properties_allocEntry(celix_properties_t* proper */ char* celix_properties_createString(celix_properties_t* properties, const char* str); +/** + * @brief Convert jansson error to celix status. + */ +celix_status_t celix_properties_jsonErrorToStatus(enum json_error_code error); + #ifdef __cplusplus } diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index b95c548e3..f28e06a3e 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -18,6 +18,7 @@ */ #include "celix_properties.h" +#include "celix_properties_private.h" #include "celix_err.h" #include "celix_stdlib_cleanup.h" @@ -561,8 +562,12 @@ celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, ce } json_auto_t* root = json_loadf(stream, jsonFlags, &jsonError); if (!root) { - celix_err_pushf("Failed to parse json: %s.", jsonError.text); - return CELIX_ILLEGAL_ARGUMENT; + celix_err_pushf("Failed to parse json from %s:%i:%i: %s.", + jsonError.source, + jsonError.line, + jsonError.column, + jsonError.text); + return celix_properties_jsonErrorToStatus(json_error_code(&jsonError)); } return celix_properties_decodeFromJson(root, decodeFlags, out); } @@ -588,3 +593,31 @@ celix_status_t celix_properties_loadFromString2(const char* input, int decodeFla fclose(stream); return status; } + +celix_status_t celix_properties_jsonErrorToStatus(enum json_error_code error) { + switch (error) { + case json_error_unknown: + return CELIX_ILLEGAL_STATE; + case json_error_out_of_memory: + case json_error_stack_overflow: + return ENOMEM; + case json_error_cannot_open_file: + return CELIX_FILE_IO_EXCEPTION; + case json_error_invalid_argument: + case json_error_invalid_utf8: + case json_error_premature_end_of_input: + case json_error_end_of_input_expected: + case json_error_invalid_syntax: + case json_error_invalid_format: + case json_error_wrong_type: + case json_error_null_character: + case json_error_null_value: + case json_error_null_byte_in_key: + case json_error_duplicate_key:; + case json_error_numeric_overflow: + case json_error_item_not_found: + case json_error_index_out_of_range: + default: + return CELIX_ILLEGAL_ARGUMENT; + } +} From a6a48e5f88f1a685cd946235f882be50b49f4311 Mon Sep 17 00:00:00 2001 From: PengZheng Date: Tue, 21 May 2024 21:01:35 +0800 Subject: [PATCH 23/25] gh-685: Avoid insertion into non-object and add test for key collision. --- .../gtest/src/PropertiesEncodingTestSuite.cc | 27 ++++++++++++++++++- libs/utils/src/properties_encoding.c | 9 ++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 2ebdf52db..b25d12603 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -1103,4 +1103,29 @@ TEST_F(PropertiesSerializationTestSuite, JsonErrorToCelixStatusTest) { EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_numeric_overflow)); EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_item_not_found)); EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_index_out_of_range)); -} \ No newline at end of file +} + +TEST_F(PropertiesSerializationTestSuite, KeyCollision) { + celix_autoptr(celix_properties_t) props = celix_properties_create(); + // pick keys such that key1 appears before key2 when iterating over the properties + celix_properties_set(props, "a.b.haha.arbifdadfsfa", "value1"); + celix_properties_set(props, "a.b.haha", "value2"); + + celix_autofree char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, + &output); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + celix_autoptr(celix_properties_t) props2 = celix_properties_create(); + // pick keys such that key1 appears before key2 when iterating over the properties + celix_properties_set(props2, "a.b.c", "value1"); + celix_properties_set(props2, "a.b.c.d", "value2"); + status = celix_properties_saveToString(props2, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, + &output); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + status = celix_properties_saveToString(props2, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output); + // "a.b.c.d" is silently discarded + EXPECT_STREQ(R"({"a":{"b":{"c":"value1"}}})", output); + std::cout << output << std::endl; + EXPECT_EQ(CELIX_SUCCESS, status); +} diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index f28e06a3e..dd866a500 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -211,9 +211,12 @@ static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_prop return ENOMEM; } json_t* subObj = json_object_get(jsonObj, name); - if (subObj && !json_is_object(subObj) && flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { - celix_err_pushf("Invalid key collision. Key '%s' already exists.", name); - return CELIX_ILLEGAL_ARGUMENT; + if (subObj && !json_is_object(subObj)) { + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { + celix_err_pushf("Invalid key collision. Key '%s' already exists.", name); + return CELIX_ILLEGAL_ARGUMENT; + } + return CELIX_SUCCESS; } if (!subObj) { subObj = json_object(); From 83682fd52919b0aaee3f286a8992fe484b825bd1 Mon Sep 17 00:00:00 2001 From: PengZheng Date: Thu, 23 May 2024 22:29:36 +0800 Subject: [PATCH 24/25] gh-685: Encode list containing NAN/INF and some minor documentation improvements. --- ...opertiesEncodingErrorInjectionTestSuite.cc | 2 +- .../gtest/src/PropertiesEncodingTestSuite.cc | 30 +++++++++++++++++++ libs/utils/include/celix/Properties.h | 2 +- libs/utils/include/celix_properties.h | 16 +++++----- libs/utils/src/properties_encoding.c | 24 +++++++++++++-- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc index 28590e939..9a5c15f7e 100644 --- a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc @@ -195,7 +195,7 @@ TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeArrayErrorTest) { EXPECT_EQ(ENOMEM, status); // And I expect 3 error message in celix_err - EXPECT_EQ(3, celix_err_getErrorCount()); + EXPECT_EQ(4, celix_err_getErrorCount()); celix_err_printErrors(stderr, "Test Error: ", "\n"); } diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index b25d12603..2d7348108 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -114,6 +114,36 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) } } +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsContainingNaNAndInfValueTest) { + auto keys = {"NAN", "INF", "-INF"}; + for (const auto& key : keys) { + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_autoptr(celix_array_list_t) list = celix_arrayList_createDoubleArray(); + celix_arrayList_addDouble(list, strtod(key, nullptr)); + celix_properties_assignArrayList(props, key, celix_steal_ptr(list)); + + // Then saving the properties to a string succeeds, but value is not added to the JSON (because JSON does not + // support NAN, INF and -INF) + celix_autofree char* output; + auto status = celix_properties_saveToString(props, 0, &output); + ASSERT_EQ(CELIX_SUCCESS, status); + EXPECT_STREQ("{}", output); + + //And saving the properties to a string with the flag CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF fails + celix_err_resetErrors(); + char* output2; + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, &output2); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + //And an error msg is added to celix_err + EXPECT_EQ(2, celix_err_getErrorCount()); + + celix_err_resetErrors(); + char* output3; + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output3); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + EXPECT_EQ(1, celix_err_getErrorCount()); + } +} TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { // Given a properties object with array list values diff --git a/libs/utils/include/celix/Properties.h b/libs/utils/include/celix/Properties.h index 166a59bb0..a924946f4 100644 --- a/libs/utils/include/celix/Properties.h +++ b/libs/utils/include/celix/Properties.h @@ -1054,7 +1054,7 @@ namespace celix { /** * @brief Load a Properties object from a file. * - * @warning The name if temporary and will be renamed to celix::Properties::load in the future (when + * @warning The name is temporary and will be renamed to celix::Properties::load in the future (when * the current celix::Properties::load is removed). * * The content of the filename file is expected to be in the format of a JSON object. diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index 5ceec16b4..f49d4628b 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -57,7 +57,7 @@ extern "C" { */ typedef enum celix_properties_value_type { CELIX_PROPERTIES_VALUE_TYPE_UNSET = 0, /**< Property value is not set. */ - CELIX_PROPERTIES_VALUE_TYPE_STRING = 1, /**< Property value is a string. */ + CELIX_PROPERTIES_VALUE_TYPE_STRING = 1, /**< Property value is a UTF-8 encoded string. */ CELIX_PROPERTIES_VALUE_TYPE_LONG = 2, /**< Property value is a long integer. */ CELIX_PROPERTIES_VALUE_TYPE_DOUBLE = 3, /**< Property value is a double. */ CELIX_PROPERTIES_VALUE_TYPE_BOOL = 4, /**< Property value is a boolean. */ @@ -1087,7 +1087,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_prop * * @param[in] properties The properties object to encode. * @param[in] filename The file to write the JSON representation of the properties object to. - * @param[in] encodeFlags The flags to use when encoding the input string. + * @param[in] encodeFlags The flags to use when encoding the input properties. * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be * encoded to a JSON representation and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file * could not be opened or written to. @@ -1105,7 +1105,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const celix_properties_t * The default encoding style is a compact and flat JSON representation. * * @param[in] properties The properties object to encode. - * @param[in] encodeFlags The flags to use when encoding the input string. + * @param[in] encodeFlags The flags to use when encoding the input properties. * @param[out] out The JSON string representation of the properties object. The caller is responsible for freeing the * returned string using free. * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be @@ -1183,8 +1183,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_prop * * Note that empty keys are valid in JSON and valid in properties, but not always desired. * - * If this flag is set, the decoding will fail if the input contains an empty key and if this flag is not set, the - * decoding will not fail and the JSON empty key entry will be ignored. + * If this flag is set, the decoding will fail if the input contains an empty key. */ #define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x20 @@ -1232,7 +1231,8 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_prop * @param[out] out The properties object that will be created from the input string. The caller is responsible for * freeing the returned properties object using celix_properties_destroy. * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be - * decoded to a properties object and ENOMEM if there was not enough memory. + * decoded to a properties object and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file + * could not be read. */ CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, @@ -1241,7 +1241,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, /** * @brief Load properties from a file. * - * @warning The name if temporary and will be renamed to celix_properties_load in the future (when + * @warning The name is temporary and will be renamed to celix_properties_load in the future (when * the current celix_properties_load is removed). * * The content of the filename file is expected to be in the format of a JSON object. @@ -1266,7 +1266,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename, /** * @brief Load properties from a string. * - * @warning The name if temporary and will be renamed to celix_properties_loadFromString in the future (when + * @warning The name is temporary and will be renamed to celix_properties_loadFromString in the future (when * the current celix_properties_loadFromString is removed). * * The input string is expected to be in the format of a JSON object. diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index dd866a500..53af5ec4b 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -50,6 +50,7 @@ static celix_status_t celix_properties_versionToJson(const celix_version_t* vers static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType, celix_array_list_entry_t entry, + int flags, json_t** out) { *out = NULL; switch (elType) { @@ -60,6 +61,13 @@ static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_ *out = json_integer(entry.longVal); break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE: + if (isnan(entry.doubleVal) || isinf(entry.doubleVal)) { + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) { + celix_err_push("Invalid NaN or Inf."); + return CELIX_ILLEGAL_ARGUMENT; + } + return CELIX_SUCCESS; // ignore NaN and Inf + } *out = json_real(entry.doubleVal); break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL: @@ -99,12 +107,14 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, return ENOMEM; } - for (int i = 0; i < celix_arrayList_size(entry->typed.arrayValue); ++i) { + int size = celix_arrayList_size(entry->typed.arrayValue); + celix_array_list_element_type_t elType = celix_arrayList_getElementType(entry->typed.arrayValue); + for (int i = 0; i < size; ++i) { celix_array_list_entry_t arrayEntry = celix_arrayList_getEntry(entry->typed.arrayValue, i); - celix_array_list_element_type_t elType = celix_arrayList_getElementType(entry->typed.arrayValue); json_t* jsonValue; - celix_status_t status = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry, &jsonValue); + celix_status_t status = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry, flags, &jsonValue); if (status != CELIX_SUCCESS) { + celix_err_pushf("Failed to encode array element(%d) for key %s.", i, key); return status; } else if (!jsonValue) { // ignore unset values @@ -117,6 +127,14 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, } } + if (json_array_size(array) == 0) { + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) { + celix_err_pushf("Invalid empty array for key %s.", key); + return CELIX_ILLEGAL_ARGUMENT; + } + return CELIX_SUCCESS; // empty array -> treat as unset property + } + *out = celix_steal_ptr(array); return CELIX_SUCCESS; } From 2eed6936accc04cfcd255ac2c13b45d898816f9c Mon Sep 17 00:00:00 2001 From: Pepijn Noltes Date: Thu, 23 May 2024 22:02:35 +0200 Subject: [PATCH 25/25] gh-685: Improve exception usage in C++ properties store/load --- CHANGES.md | 2 + .../include/celix/FrameworkExceptions.h | 2 +- .../include/celix/ScheduledEventBuilder.h | 4 +- libs/utils/gtest/CMakeLists.txt | 1 + .../utils/gtest/src/CxxExceptionsTestSuite.cc | 52 ++++++++++++++ .../gtest/src/PropertiesEncodingTestSuite.cc | 8 +-- libs/utils/include/celix/Exception.h | 33 --------- libs/utils/include/celix/Exceptions.h | 71 +++++++++++++++++++ libs/utils/include/celix/Filter.h | 2 +- libs/utils/include/celix/IOException.h | 51 ------------- libs/utils/include/celix/Properties.h | 26 ++++--- 11 files changed, 149 insertions(+), 103 deletions(-) create mode 100644 libs/utils/gtest/src/CxxExceptionsTestSuite.cc delete mode 100644 libs/utils/include/celix/Exception.h create mode 100644 libs/utils/include/celix/Exceptions.h delete mode 100644 libs/utils/include/celix/IOException.h diff --git a/CHANGES.md b/CHANGES.md index 8b3044037..2720590a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -71,6 +71,8 @@ limitations under the License. Celix event thread. - Apache Celix filter now use the underlying properties value types for matching. This means that it is more important to add service properties with the correct type. +- Celix C++ Exception are now defined in the `celix/exceptions.h` header file. The `celix/Exception.h` + and `celix/IOException.h` are removed. ## New Features diff --git a/libs/framework/include/celix/FrameworkExceptions.h b/libs/framework/include/celix/FrameworkExceptions.h index d6fd2cdab..14b35a2dc 100644 --- a/libs/framework/include/celix/FrameworkExceptions.h +++ b/libs/framework/include/celix/FrameworkExceptions.h @@ -18,7 +18,7 @@ */ #pragma once -#include "celix/Exception.h" +#include "celix/Exceptions.h" namespace celix { diff --git a/libs/framework/include/celix/ScheduledEventBuilder.h b/libs/framework/include/celix/ScheduledEventBuilder.h index c93a93bdb..47cef480d 100644 --- a/libs/framework/include/celix/ScheduledEventBuilder.h +++ b/libs/framework/include/celix/ScheduledEventBuilder.h @@ -22,8 +22,8 @@ #include #include +#include "celix/Exceptions.h" #include "celix/ScheduledEvent.h" -#include "celix/Exception.h" namespace celix { @@ -112,7 +112,7 @@ class ScheduledEventBuilder final { */ ScheduledEvent build() { if (!callback) { - throw celix::Exception{"Cannot build scheduled event without callback"}; //TODO improve error + throw celix::Exception{"Cannot build scheduled event without callback"}; } return ScheduledEvent{ctx, name, std::move(callback), std::move(removeCallback), options}; } diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt index 48f820a66..0d717e1fa 100644 --- a/libs/utils/gtest/CMakeLists.txt +++ b/libs/utils/gtest/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(test_utils src/CelixUtilsAutoCleanupTestSuite.cc src/ArrayListTestSuite.cc src/DeprecatedHashmapTestSuite.cc + src/CxxExceptionsTestSuite.cc ) target_link_libraries(test_utils PRIVATE utils_cut Celix::utils GTest::gtest GTest::gtest_main libzip::zip) diff --git a/libs/utils/gtest/src/CxxExceptionsTestSuite.cc b/libs/utils/gtest/src/CxxExceptionsTestSuite.cc new file mode 100644 index 000000000..9ff07420e --- /dev/null +++ b/libs/utils/gtest/src/CxxExceptionsTestSuite.cc @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix/Exceptions.h" + +class ExceptionsTestSuite : public ::testing::Test { +public: +}; + + +TEST_F(ExceptionsTestSuite, ThrowExceptionTest) { + EXPECT_THROW(celix::impl::throwException(CELIX_ILLEGAL_ARGUMENT, "Test"), celix::IllegalArgumentException); + try { + celix::impl::throwException(CELIX_ILLEGAL_ARGUMENT, "Test"); + } catch (const celix::IllegalArgumentException& ex) { + EXPECT_STREQ("Test (Illegal argument)", ex.what()); + } + + EXPECT_THROW(celix::impl::throwException(CELIX_FILE_IO_EXCEPTION, "Test"), celix::IOException); + try { + celix::impl::throwException(CELIX_FILE_IO_EXCEPTION, "Test"); + } catch (const celix::IOException& ex) { + EXPECT_STREQ("Test (File I/O exception)", ex.what()); + } + + //Not all celix_status_t values are mapped and in that case the default Exception is thrown + EXPECT_THROW(celix::impl::throwException(CELIX_FRAMEWORK_EXCEPTION, "Test"), celix::Exception); + try { + celix::impl::throwException(CELIX_FRAMEWORK_EXCEPTION, "Test"); + } catch (const celix::Exception& ex) { + EXPECT_STREQ("Test (Framework exception)", ex.what()); + } +} + diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index 2d7348108..fedec728a 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -504,14 +504,14 @@ TEST_F(PropertiesSerializationTestSuite, SaveCxxPropertiesTest) { // When saving the properties to a string using an errors on duplicate key flag EXPECT_THROW(props.saveToString(celix::Properties::EncodingFlags::Strict), - celix::IOException); + celix::IllegalArgumentException); // When saving the properties to a string using combined flags EXPECT_THROW(props.saveToString( celix::Properties::EncodingFlags::Pretty | celix::Properties::EncodingFlags::ErrorOnEmptyArrays | celix::Properties::EncodingFlags::ErrorOnCollisions | celix::Properties::EncodingFlags::ErrorOnNanInf | celix::Properties::EncodingFlags::NestedStyle), - celix::IOException); + celix::IllegalArgumentException); // When saving the properties to an invalid filename location EXPECT_THROW(props.save("/non-existing/no/rights/file.json"), @@ -1025,7 +1025,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadCxxPropertiesTest) { // When loading the properties from the JSON object with a strict flag EXPECT_THROW(celix::Properties::loadFromString(jsonInput, celix::Properties::DecodeFlags::Strict), - celix::IOException); + celix::IllegalArgumentException); // When loading the properties from the JSON object with a flag combined EXPECT_THROW( @@ -1035,7 +1035,7 @@ TEST_F(PropertiesSerializationTestSuite, LoadCxxPropertiesTest) { celix::Properties::DecodeFlags::ErrorOnEmptyArrays | celix::Properties::DecodeFlags::ErrorOnEmptyKeys | celix::Properties::DecodeFlags::ErrorOnUnsupportedArrays | celix::Properties::DecodeFlags::ErrorOnNullValues | celix::Properties::DecodeFlags::ErrorOnNullValues), - celix::IOException); + celix::IllegalArgumentException); EXPECT_THROW(celix::Properties::load2("non_existing_file.json"), celix::IOException); } diff --git a/libs/utils/include/celix/Exception.h b/libs/utils/include/celix/Exception.h deleted file mode 100644 index 481d6f862..000000000 --- a/libs/utils/include/celix/Exception.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -#pragma once - -#include - -namespace celix { - - /** - * @brief Celix runtime Exception - */ - class Exception : public std::runtime_error { - public: - using std::runtime_error::runtime_error; - }; - -} diff --git a/libs/utils/include/celix/Exceptions.h b/libs/utils/include/celix/Exceptions.h new file mode 100644 index 000000000..d6d24a104 --- /dev/null +++ b/libs/utils/include/celix/Exceptions.h @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#pragma once + +#include "celix_errno.h" + +#include +#include + +namespace celix { + + /** + * @brief Celix Generic Exception + */ + class Exception : public std::runtime_error { + public: + using std::runtime_error::runtime_error; + }; + + /** + * @brief Celix Illegal Argument Exception + */ + class IllegalArgumentException final: public Exception { + public: + using Exception::Exception; + }; + + /** + * @brief Celix IO Exception + */ + class IOException final: public Exception { + public: + using Exception::Exception; + }; + + namespace impl { + /** + * @brief Utils function to throw a celix::Exception using the given celix_status_t and message. + */ + void inline throwException(celix_status_t status, const std::string& message) { + assert(status != CELIX_SUCCESS); + const auto* statusMsg = celix_strerror(status); + auto msg = std::string{message} + " (" + statusMsg + ")"; + switch (status) { + case CELIX_ILLEGAL_ARGUMENT: + throw celix::IllegalArgumentException{msg}; + case CELIX_FILE_IO_EXCEPTION: + throw celix::IOException{msg}; + default: + throw celix::Exception{msg}; + } + } + } + +} diff --git a/libs/utils/include/celix/Filter.h b/libs/utils/include/celix/Filter.h index aecda6d8f..7a565a934 100644 --- a/libs/utils/include/celix/Filter.h +++ b/libs/utils/include/celix/Filter.h @@ -25,7 +25,7 @@ #include "celix_filter.h" #include "celix_err.h" -#include "celix/Exception.h" +#include "celix/Exceptions.h" #include "celix/Properties.h" namespace celix { diff --git a/libs/utils/include/celix/IOException.h b/libs/utils/include/celix/IOException.h deleted file mode 100644 index 1715801e3..000000000 --- a/libs/utils/include/celix/IOException.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -#pragma once - -#include -#if __cplusplus >= 201703L //C++17 or higher -#include -#endif - -namespace celix { - - /** - * @brief Celix runtime IO Exception - */ - class IOException : public std::exception { - public: -#if __cplusplus >= 201703L //C++17 or higher - explicit IOException(std::string_view msg) : w{msg} {} -#else - explicit IOException(std::string msg) : w{std::move(msg)} {} -#endif - - IOException(const IOException&) = default; - IOException(IOException&&) = default; - IOException& operator=(const IOException&) = default; - IOException& operator=(IOException&&) = default; - - [[nodiscard]] const char* what() const noexcept override { - return w.c_str(); - } - private: - std::string w; - }; - -} diff --git a/libs/utils/include/celix/Properties.h b/libs/utils/include/celix/Properties.h index a924946f4..ee71925ea 100644 --- a/libs/utils/include/celix/Properties.h +++ b/libs/utils/include/celix/Properties.h @@ -30,7 +30,7 @@ #include "celix_properties.h" #include "celix_utils.h" #include "celix/Version.h" -#include "celix/IOException.h" +#include "celix/Exceptions.h" namespace celix { @@ -982,6 +982,7 @@ namespace celix { * @param[in] filename The file to write the JSON representation of the properties object to. * @param[in] encodingFlags The flags to use when encoding the input string. * @throws celix::IOException If an error occurs while writing to the file. + * @throws celix::IllegalArgumentException If the provided properties cannot be encoded to JSON. * @throws std::bad_alloc If there was not enough memory to save the properties. */ void save(const std::string& filename, EncodingFlags encodingFlags = EncodingFlags::None) const { @@ -989,7 +990,7 @@ namespace celix { if (status == ENOMEM) { throw std::bad_alloc(); } else if (status != CELIX_SUCCESS) { - throw celix::IOException{"Cannot save celix::Properties to " + filename}; + celix::impl::throwException(status, std::string{"Cannot save celix::Properties to "} + filename); } } @@ -1002,7 +1003,7 @@ namespace celix { * The default encoding style is a compact and flat JSON representation. * * @param[in] encodeFlags The flags to use when encoding the input string. - * @throws celix::IOException If an error occurs while writing to the file. + * @throws celix::IllegalArgumentException If the provided properties cannot be encoded to JSON. * @throws std::bad_alloc If there was not enough memory to save the properties. */ std::string saveToString(EncodingFlags encodeFlags = EncodingFlags::None) const { @@ -1011,7 +1012,7 @@ namespace celix { if (status == ENOMEM) { throw std::bad_alloc(); } else if (status != CELIX_SUCCESS) { - throw celix::IOException{"Cannot save celix::Properties to string"}; + celix::impl::throwException(status, "Cannot save celix::Properties to string"); } std::string result{str}; free(str); @@ -1048,6 +1049,8 @@ namespace celix { * @param[in] path The path to the file containing the properties. * @return A new Properties object containing the properties from the file. * @throws celix::IOException If the file cannot be opened or read. + * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object. + * @throws std::bad_alloc If there was not enough memory to load the properties. */ static Properties load(const std::string& path) { return loadFrom(path.data()); } @@ -1066,6 +1069,7 @@ namespace celix { * @param[in] decodeFlags The flags to use when decoding the input string. * @return A new Properties object containing the properties from the file. * @throws celix::IOException If the file cannot be opened or read. + * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object. * @throws std::bad_alloc If there was not enough memory to load the properties. */ static Properties load2(const std::string& filename, DecodeFlags decodeFlags = DecodeFlags::None) { @@ -1074,7 +1078,7 @@ namespace celix { if (status == ENOMEM) { throw std::bad_alloc(); } else if (status != CELIX_SUCCESS) { - throw celix::IOException{"Cannot load celix::Properties from " + filename}; + celix::impl::throwException(status, "Cannot load celix::Properties from " + filename); } return celix::Properties::own(props); } @@ -1090,22 +1094,22 @@ namespace celix { * * @param[in] input The input string to parse. * @param[in] decodeFlags The flags to use when decoding the input string. - * @return A new Properties object containing the properties from the file. - * @throws celix::IOException If the file cannot be opened or read. - * @throws std::bad_alloc If there was not enough memory to load the properties. - */ + * @return A new Properties object containing the properties from the file. + * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object. + * @throws std::bad_alloc If there was not enough memory to load the properties. + */ static Properties loadFromString(const std::string& input, DecodeFlags decodeFlags = DecodeFlags::None) { celix_properties_t* props; auto status = celix_properties_loadFromString2(input.c_str(), static_cast(decodeFlags), &props); if (status == ENOMEM) { throw std::bad_alloc(); } else if (status != CELIX_SUCCESS) { - throw celix::IOException{"Cannot load celix::Properties from string"}; + celix::impl::throwException(status, "Cannot load celix::Properties from string"); } return celix::Properties::own(props); } - private: + private: Properties(celix_properties_t* props, bool takeOwnership) : cProps{props, [takeOwnership](celix_properties_t* p){ if (takeOwnership) { celix_properties_destroy(p); }}} {}