add source/sink facility for properties - fixes #430

Sun, 13 Oct 2024 16:44:29 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 13 Oct 2024 16:44:29 +0200
changeset 928
d2d42cb1d59e
parent 927
71e7e9ba4b97
child 929
192a440b99df

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;
 }

mercurial