Sun, 13 Oct 2024 16:44:29 +0200
add source/sink facility for properties - fixes #430
CHANGELOG | file | annotate | diff | comparison | revisions | |
src/cx/properties.h | file | annotate | diff | comparison | revisions | |
src/properties.c | file | annotate | diff | comparison | revisions | |
tests/test_properties.c | file | annotate | diff | comparison | revisions |
--- a/CHANGELOG Sun Oct 13 15:19:12 2024 +0200 +++ b/CHANGELOG Sun Oct 13 16:44:29 2024 +0200 @@ -1,5 +1,6 @@ Version 3.1 - tbd. ------------------------ + * adds properties.h * adds tree.h * adds cxIterator() to create iterators over raw C arrays * adds cx_array_default_reallocator
--- a/src/cx/properties.h Sun Oct 13 15:19:12 2024 +0200 +++ b/src/cx/properties.h Sun Oct 13 16:44:29 2024 +0200 @@ -38,8 +38,11 @@ #include "common.h" #include "string.h" +#include "map.h" #include "array_list.h" +#include <stdio.h> + struct cx_properties_config_s { /** * The key/value delimiter that shall be used. @@ -102,6 +105,15 @@ */ CX_PROPERTIES_INCOMPLETE_DATA, /** + * Not used as a status and never returned by any function. + * + * You can use this enumerator to check for all "good" status results + * by checking if the status is less than \c CX_PROPERTIES_OK. + * + * A "good" status means, that you can refill data and continue parsing. + */ + CX_PROPERTIES_OK, + /** * Input buffer is \c NULL. */ CX_PROPERTIES_NULL_INPUT, @@ -117,9 +129,32 @@ * More internal buffer was needed, but could not be allocated. */ CX_PROPERTIES_BUFFER_ALLOC_FAILED, + /** + * Initializing the properties source failed. + * + * @see cx_properties_read_init_func + */ + CX_PROPERTIES_READ_INIT_FAILED, + /** + * Reading from a properties source failed. + * + * @see cx_properties_read_func + */ + CX_PROPERTIES_READ_FAILED, + /** + * Sinking a k/v-pair failed. + * + * @see cx_properties_sink_func + */ + CX_PROPERTIES_SINK_FAILED, }; /** + * Typedef for the properties status enum. + */ +typedef enum cx_properties_status CxPropertiesStatus; + +/** * Interface for working with properties data. */ struct cx_properties_s { @@ -171,6 +206,134 @@ /** + * Typedef for a properties sink. + */ +typedef struct cx_properties_sink_s CxPropertiesSink; + +/** + * A function that consumes a k/v-pair in a sink. + * + * The sink could be e.g. a map and the sink function would be calling + * a map function to store the k/v-pair. + * + * @param prop the properties interface that wants to sink a k/v-pair + * @param sink the sink + * @param key the key + * @param value the value + * @return zero on success, non-zero when sinking the k/v-pair failed + */ +__attribute__((__nonnull__)) +typedef int(*cx_properties_sink_func)( + CxProperties *prop, + CxPropertiesSink *sink, + cxstring key, + cxstring value +); + +/** + * Defines a sink for k/v-pairs. + */ +struct cx_properties_sink_s { + /** + * The sink object. + */ + void *sink; + /** + * Optional custom data. + */ + void *data; + /** + * A function for consuming k/v-pairs into the sink. + */ + cx_properties_sink_func sink_func; +}; + + +/** + * Typedef for a properties source. + */ +typedef struct cx_properties_source_s CxPropertiesSource; + +/** + * A function that reads data from a source. + * + * When the source is depleted, implementations SHALL provide an empty + * string in the \p target and return zero. + * A non-zero return value is only permitted in case of an error. + * + * The meaning of the optional parameters is implementation-dependent. + * + * @param prop the properties interface that wants to read from the source + * @param src the source + * @param target a string buffer where the read data shall be stored + * @return zero on success, non-zero when reading data failed + */ + __attribute__((__nonnull__)) +typedef int(*cx_properties_read_func)( + CxProperties *prop, + CxPropertiesSource *src, + cxstring *target +); + + /** + * A function that may initialize additional memory for the source. + * + * @param prop the properties interface that wants to read from the source + * @param src the source + * @return zero when initialization was successful, non-zero otherwise + */ + __attribute__((__nonnull__)) +typedef int(*cx_properties_read_init_func)( + CxProperties *prop, + CxPropertiesSource *src +); + +/** + * A function that cleans memory initialized by the read_init_func. + * + * @param prop the properties interface that wants to read from the source + * @param src the source + */ +__attribute__((__nonnull__)) +typedef void(*cx_properties_read_clean_func)( + CxProperties *prop, + CxPropertiesSource *src +); + +/** + * Defines a properties source. + */ +struct cx_properties_source_s { + /** + * The source object. + * + * For example a file stream or a string. + */ + void *src; + /** + * Optional additional data pointer. + */ + void *data_ptr; + /** + * Optional size information. + */ + size_t data_size; + /** + * A function that reads data from the source. + */ + cx_properties_read_func read_func; + /** + * Optional function that may prepare the source for reading data. + */ + cx_properties_read_init_func read_init_func; + /** + * Optional function that cleans additional memory allocated by the + * read_init_func. + */ + cx_properties_read_clean_func read_clean_func; +}; + +/** * Initialize a properties interface. * * @param prop the properties interface @@ -251,6 +414,7 @@ * @param buf a pointer to stack memory * @param capacity the capacity of the stack memory */ +__attribute__((__nonnull__)) void cxPropertiesUseStack( CxProperties *prop, char *buf, @@ -277,10 +441,95 @@ * @return the status code as defined above * @see cxPropertiesFill() */ -enum cx_properties_status cxPropertiesNext( +__attribute__((__nonnull__, __warn_unused_result__)) +CxPropertiesStatus cxPropertiesNext( CxProperties *prop, cxstring *key, cxstring *value ); +/** + * Creates a properties sink for an UCX map. + * + * The values stored in the map will be pointers to strings allocated + * by #cx_strdup_a(). + * The default stdlib allocator will be used, unless you specify a custom + * allocator in the optional \c data of the sink. + * + * @param map the map that shall consume the k/v-pairs. + * @return the sink + * @see cxPropertiesLoad() + */ +__attribute__((__nonnull__, __warn_unused_result__)) +CxPropertiesSink cxPropertiesMapSink(CxMap *map); + +/** + * Creates a properties source based on an UCX string. + * + * @param str the string + * @return the properties source + * @see cxPropertiesLoad() + */ +__attribute__((__warn_unused_result__)) +CxPropertiesSource cxPropertiesStringSource(cxstring str); + +/** + * Creates a properties source based on C string with the specified length. + * + * @param str the string + * @param len the length + * @return the properties source + * @see cxPropertiesLoad() + */ +__attribute__((__nonnull__, __warn_unused_result__)) +CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len); + +/** + * Creates a properties source based on a C string. + * + * The length will be determined with strlen(), so the string MUST be + * zero-terminated. + * + * @param str the string + * @return the properties source + * @see cxPropertiesLoad() + */ +__attribute__((__nonnull__, __warn_unused_result__)) +CxPropertiesSource cxPropertiesCstrSource(const char *str); + +/** + * Creates a properties source based on an FILE. + * + * @param file the file + * @param chunk_size how many bytes may be read in one operation + * + * @return the properties source + * @see cxPropertiesLoad() + */ +__attribute__((__nonnull__, __warn_unused_result__)) +CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size); + + +/** + * Loads properties data from a source and transfers it to a sink. + * + * This function tries to read as much data from the source as possible. + * When the source was completely consumed and at least on k/v-pair was found, + * the return value will be #CX_PROPERTIES_NO_ERROR. + * When the source was consumed but no k/v-pairs were found, the return value + * will be #CX_PROPERTIES_NO_DATA. + * The other result codes apply, according to their description. + * + * @param prop the properties interface + * @param sink the sink + * @param source the source + * @return the status of the last operation + */ +__attribute__((__nonnull__)) +CxPropertiesStatus cxPropertiesLoad( + CxProperties *prop, + CxPropertiesSink sink, + CxPropertiesSource source +); + #endif // UCX_PROPERTIES
--- a/src/properties.c Sun Oct 13 15:19:12 2024 +0200 +++ b/src/properties.c Sun Oct 13 16:44:29 2024 +0200 @@ -92,6 +92,7 @@ char *dest = prop->buf + prop->buf_size; memcpy(dest, src, len); prop->buf_size += len; + prop->text_pos += len; return 0; } @@ -137,7 +138,7 @@ prop->flags |= CX_PROPERTIES_FLAG_USE_STACK; } -enum cx_properties_status cxPropertiesNext( +CxPropertiesStatus cxPropertiesNext( CxProperties *prop, cxstring *key, cxstring *value @@ -171,7 +172,7 @@ prop->text_pos = 0; prop->buf_size = 0; - enum cx_properties_status result; + CxPropertiesStatus result; result = cxPropertiesNext(prop, key, value); // restore original buffer @@ -199,6 +200,9 @@ } } else { // still not enough data + if (cx_properties_rescue_input(prop)) { + return CX_PROPERTIES_BUFFER_ALLOC_FAILED; + } return CX_PROPERTIES_INCOMPLETE_DATA; } } @@ -241,6 +245,9 @@ if (c != '\n') { // we don't have enough data for a line + if (cx_properties_rescue_input(prop)) { + return CX_PROPERTIES_BUFFER_ALLOC_FAILED; + } return CX_PROPERTIES_INCOMPLETE_DATA; } @@ -284,3 +291,167 @@ assert(prop->text_pos == prop->text_size); return CX_PROPERTIES_NO_DATA; } + +static int cx_properties_sink_map( + __attribute__((__unused__)) CxProperties *prop, + CxPropertiesSink *sink, + cxstring key, + cxstring value +) { + CxMap *map = sink->sink; + CxAllocator *alloc = sink->data; + cxmutstr v = cx_strdup_a(alloc, value); + int r = cx_map_put_cxstr(map, key, v.ptr); + if (r != 0) cx_strfree_a(alloc, &v); + return r; +} + +CxPropertiesSink cxPropertiesMapSink(CxMap *map) { + CxPropertiesSink sink; + sink.sink = map; + sink.data = cxDefaultAllocator; + sink.sink_func = cx_properties_sink_map; + return sink; +} + +static int cx_properties_read_string( + CxProperties *prop, + CxPropertiesSource *src, + cxstring *target +) { + if (prop->text == src->src) { + // when the input buffer already contains the string + // we have nothing more to provide + target->length = 0; + } else { + target->ptr = src->src; + target->length = src->data_size; + } + return 0; +} + +static int cx_properties_read_file( + __attribute__((__unused__)) CxProperties *prop, + CxPropertiesSource *src, + cxstring *target +) { + target->ptr = src->data_ptr; + target->length = fread(src->data_ptr, 1, src->data_size, src->src); + return ferror(src->src); +} + +static int cx_properties_read_init_file( + __attribute__((__unused__)) CxProperties *prop, + CxPropertiesSource *src +) { + src->data_ptr = malloc(src->data_size); + if (src->data_ptr == NULL) return 1; + return 0; +} + +static void cx_properties_read_clean_file( + __attribute__((__unused__)) CxProperties *prop, + CxPropertiesSource *src +) { + free(src->data_ptr); +} + +CxPropertiesSource cxPropertiesStringSource(cxstring str) { + CxPropertiesSource src; + src.src = (void*) str.ptr; + src.data_size = str.length; + src.read_func = cx_properties_read_string; + src.read_init_func = NULL; + src.read_clean_func = NULL; + return src; +} + +CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len) { + CxPropertiesSource src; + src.src = (void*) str; + src.data_size = len; + src.read_func = cx_properties_read_string; + src.read_init_func = NULL; + src.read_clean_func = NULL; + return src; +} + +CxPropertiesSource cxPropertiesCstrSource(const char *str) { + CxPropertiesSource src; + src.src = (void*) str; + src.data_size = strlen(str); + src.read_func = cx_properties_read_string; + src.read_init_func = NULL; + src.read_clean_func = NULL; + return src; +} + +CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size) { + CxPropertiesSource src; + src.src = file; + src.data_size = chunk_size; + src.read_func = cx_properties_read_file; + src.read_init_func = cx_properties_read_init_file; + src.read_clean_func = cx_properties_read_clean_file; + return src; +} + +CxPropertiesStatus cxPropertiesLoad( + CxProperties *prop, + CxPropertiesSink sink, + CxPropertiesSource source +) { + assert(source.read_func != NULL); + assert(sink.sink_func != NULL); + + // initialize reader + if (source.read_init_func != NULL) { + if (source.read_init_func(prop, &source)) { + return CX_PROPERTIES_READ_INIT_FAILED; + } + } + + // transfer the data from the source to the sink + CxPropertiesStatus status; + bool found = false; + while (true) { + // read input + cxstring input; + if (source.read_func(prop, &source, &input)) { + status = CX_PROPERTIES_READ_FAILED; + break; + } + + // no more data - break + if (input.length == 0) { + status = found ? CX_PROPERTIES_NO_ERROR : CX_PROPERTIES_NO_DATA; + break; + } + + // set the input buffer and read the k/v-pairs + cxPropertiesInput(prop, input.ptr, input.length); + + CxPropertiesStatus kv_status; + do { + cxstring key, value; + kv_status = cxPropertiesNext(prop, &key, &value); + if (kv_status == CX_PROPERTIES_NO_ERROR) { + found = true; + if (sink.sink_func(prop, &sink, key, value)) { + kv_status = CX_PROPERTIES_SINK_FAILED; + } + } + } while (kv_status == CX_PROPERTIES_NO_ERROR); + + if (kv_status > CX_PROPERTIES_OK) { + status = kv_status; + break; + } + } + + if (source.read_clean_func != NULL) { + source.read_clean_func(prop, &source); + } + + return status; +}
--- a/tests/test_properties.c Sun Oct 13 15:19:12 2024 +0200 +++ b/tests/test_properties.c Sun Oct 13 16:44:29 2024 +0200 @@ -27,8 +27,10 @@ */ #include "cx/test.h" +#include "util_allocator.h" #include "cx/properties.h" +#include "cx/hash_map.h" CX_TEST(test_cx_properties_init) { CxProperties prop; @@ -89,7 +91,7 @@ CxProperties prop; cxPropertiesInitDefault(&prop); - enum cx_properties_status result; + CxPropertiesStatus result; cxstring key; cxstring value; CX_TEST_DO { @@ -155,7 +157,7 @@ CxProperties prop; cxPropertiesInitDefault(&prop); - enum cx_properties_status result; + CxPropertiesStatus result; cxstring key; cxstring value; @@ -178,7 +180,7 @@ CX_TEST(test_cx_properties_next_part) { CxProperties prop; cxPropertiesInitDefault(&prop); - enum cx_properties_status result; + CxPropertiesStatus result; cxstring key; cxstring value; const char *str; @@ -287,7 +289,7 @@ CX_TEST(test_ucx_properties_next_long_lines) { CxProperties prop; cxPropertiesInitDefault(&prop); - enum cx_properties_status result; + CxPropertiesStatus result; cxstring key; cxstring value; @@ -367,6 +369,139 @@ free(long_value); } +CX_TEST(test_cx_properties_load_string_to_map) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + CX_TEST_DO { + const char *str = "key1 = value1\nkey2 = value2\n\n#comment\n\nkey3 = value3\n"; + + CxMap *map = cxHashMapCreateSimple(CX_STORE_POINTERS); + cxDefineAdvancedDestructor(map, cxFree, alloc); + CxProperties prop; + cxPropertiesInitDefault(&prop); + CxPropertiesSink sink = cxPropertiesMapSink(map); + sink.data = alloc; // use the testing allocator + CxPropertiesSource src = cxPropertiesCstrSource(str); + CxPropertiesStatus status = cxPropertiesLoad(&prop, sink, src); + + CX_TEST_ASSERT(status == CX_PROPERTIES_NO_ERROR); + CX_TEST_ASSERT(cxMapSize(map) == 3); + + char *v1 = cxMapGet(map, "key1"); + char *v2 = cxMapGet(map, "key2"); + char *v3 = cxMapGet(map, "key3"); + + CX_TEST_ASSERTM(v1, "value for key1 not found"); + CX_TEST_ASSERTM(v2, "value for key2 not found"); + CX_TEST_ASSERTM(v3, "value for key3 not found"); + + CX_TEST_ASSERT(!strcmp(v1, "value1")); + CX_TEST_ASSERT(!strcmp(v2, "value2")); + CX_TEST_ASSERT(!strcmp(v3, "value3")); + + // second test + cxMapClear(map); + + str = "\n#comment\n"; + src = cxPropertiesCstrnSource(str, strlen(str)); + status = cxPropertiesLoad(&prop, sink, src); + + CX_TEST_ASSERT(status == CX_PROPERTIES_NO_DATA); + CX_TEST_ASSERT(cxMapSize(map) == 0); + + str = "key1 = value1\nsyntax error line\n"; + src = cxPropertiesStringSource(cx_str(str)); + status = cxPropertiesLoad(&prop, sink, src); + + CX_TEST_ASSERT(status == CX_PROPERTIES_INVALID_MISSING_DELIMITER); + + // the successfully read k/v-pair is in the map, nevertheless + CX_TEST_ASSERT(cxMapSize(map) == 1); + char *v = cxMapGet(map, "key1"); + CX_TEST_ASSERT(!strcmp(v, "value1")); + + cxMapDestroy(map); + cxPropertiesDestroy(&prop); + + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + +CX_TEST(test_cx_properties_load_file_to_map) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *alloc = &talloc.base; + CX_TEST_DO { + FILE *f = tmpfile(); + CX_TEST_ASSERTM(f, "test file cannot be opened, test aborted"); + fprintf(f, "# properties file\n\nkey1 = value1\nkey2 = value2\n"); + fprintf(f, "\n\nkey3 = value3\n\n"); + + size_t key_len = 512; + char *long_key = (char *) malloc(key_len); + memset(long_key, 'k', 512); + + size_t value_len = 2048; + char *long_value = (char *) malloc(value_len); + memset(long_value, 'v', 2048); + + fwrite(long_key, 1, key_len, f); + fprintf(f, " = "); + fwrite(long_value, 1, value_len, f); + fprintf(f, " \n"); + + fprintf(f, "\n\n\n\nlast_key = property value\n"); + + fflush(f); + fseek(f, 0, SEEK_SET); + + // preparation of test file complete + + CxMap *map = cxHashMapCreateSimple(CX_STORE_POINTERS); + cxDefineAdvancedDestructor(map, cxFree, alloc); + CxProperties prop; + cxPropertiesInitDefault(&prop); + CxPropertiesSink sink = cxPropertiesMapSink(map); + sink.data = alloc; // use the testing allocator + CxPropertiesSource src = cxPropertiesFileSource(f, 512); + CxPropertiesStatus status = cxPropertiesLoad(&prop, sink, src); + fclose(f); + + CX_TEST_ASSERT(status == CX_PROPERTIES_NO_ERROR); + CX_TEST_ASSERT(cxMapSize(map) == 5); + + char *v1 = cxMapGet(map, "key1"); + char *v2 = cxMapGet(map, cx_str("key2")); + char *v3 = cxMapGet(map, "key3"); + char *lv = cxMapGet(map, cx_strn(long_key, key_len)); + char *lk = cxMapGet(map, "last_key"); + + CX_TEST_ASSERTM(v1, "value for key1 not found"); + CX_TEST_ASSERTM(v2, "value for key2 not found"); + CX_TEST_ASSERTM(v3, "value for key3 not found"); + CX_TEST_ASSERTM(lv, "value for long key not found"); + CX_TEST_ASSERTM(lk, "value for last_key not found"); + + CX_TEST_ASSERT(!strcmp(v1, "value1")); + CX_TEST_ASSERT(!strcmp(v2, "value2")); + CX_TEST_ASSERT(!strcmp(v3, "value3")); + cxstring expected = cx_strn(long_value, value_len); + cxstring actual = cx_str(lv); + CX_TEST_ASSERT(!cx_strcmp(expected, actual)); + CX_TEST_ASSERT(!strcmp(lk, "property value")); + + free(long_key); + free(long_value); + cxMapDestroy(map); + cxPropertiesDestroy(&prop); + + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + CxTestSuite *cx_test_suite_properties(void) { CxTestSuite *suite = cx_test_suite_new("properties"); @@ -375,6 +510,8 @@ cx_test_register(suite, test_cx_properties_next_multi); cx_test_register(suite, test_cx_properties_next_part); cx_test_register(suite, test_ucx_properties_next_long_lines); + cx_test_register(suite, test_cx_properties_load_string_to_map); + cx_test_register(suite, test_cx_properties_load_file_to_map); return suite; }