add implementation for the properties parser

Sat, 12 Oct 2024 19:34:19 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 12 Oct 2024 19:34:19 +0200
changeset 924
3c90dfc35f06
parent 923
45da884269c8
child 925
fd6e191f3268

add implementation for the properties parser

relates to #429

src/Makefile file | annotate | diff | comparison | revisions
src/cx/properties.h file | annotate | diff | comparison | revisions
src/properties.c file | annotate | diff | comparison | revisions
tests/Makefile file | annotate | diff | comparison | revisions
tests/test_properties.c file | annotate | diff | comparison | revisions
tests/ucxtest.c file | annotate | diff | comparison | revisions
--- a/src/Makefile	Thu Oct 10 18:40:27 2024 +0200
+++ b/src/Makefile	Sat Oct 12 19:34:19 2024 +0200
@@ -125,7 +125,9 @@
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(build_dir)/properties$(OBJ_EXT): properties.c cx/properties.h
+$(build_dir)/properties$(OBJ_EXT): properties.c cx/properties.h \
+ cx/common.h cx/string.h cx/allocator.h cx/array_list.h cx/list.h \
+ cx/collection.h cx/iterator.h cx/compare.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
--- a/src/cx/properties.h	Thu Oct 10 18:40:27 2024 +0200
+++ b/src/cx/properties.h	Sat Oct 12 19:34:19 2024 +0200
@@ -38,6 +38,7 @@
 
 #include "common.h"
 #include "string.h"
+#include "array_list.h"
 
 struct cx_properties_config_s {
     /**
@@ -50,7 +51,7 @@
      * The character, when appearing at the end of a line, continues that line.
      * This is '\' by default.
      */
-    char continuation;
+    // char continuation; // TODO: line continuation in properties
 
     /**
      * The first comment character.
@@ -104,6 +105,18 @@
      * Input buffer is \c NULL.
      */
     CX_PROPERTIES_NULL_INPUT,
+    /**
+     * The line contains a delimiter, but no key.
+     */
+    CX_PROPERTIES_INVALID_EMPTY_KEY,
+    /**
+     * The line contains data, but no delimiter.
+     */
+    CX_PROPERTIES_INVALID_MISSING_DELIMITER,
+    /**
+     * More internal buffer was needed, but could not be allocated.
+     */
+    CX_PROPERTIES_BUFFER_ALLOC_FAILED,
 };
 
 /**
@@ -121,14 +134,34 @@
     const char *text;
 
     /**
-     * Length of the text buffer.
+     * Size of the text buffer.
      */
-    size_t text_len;
+    size_t text_size;
 
     /**
      * Position in the text buffer.
      */
     size_t text_pos;
+
+    /**
+     * Temporary internal buffer.
+     */
+    char *buf;
+
+    /**
+     * Size of the internal buffer.
+     */
+    size_t buf_size;
+
+    /**
+     * Capacity of the internal buffer.
+     */
+    size_t buf_capacity;
+
+    /**
+     * Internal flags.
+     */
+    int flags;
 };
 
 /**
@@ -138,20 +171,28 @@
 
 
 /**
- * Initialize a properties parser.
+ * Initialize a properties interface.
  *
  * @param prop the properties interface
  * @param config the properties configuration
  * @see cxPropertiesInitDefault()
  */
- __attribute__((__nonnull__))
-static inline void cxPropertiesInit(
-        CxProperties *prop,
-        CxPropertiesConfig config
-) {
-     prop->config = config;
-     prop->text = NULL;
- }
+__attribute__((__nonnull__))
+void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
+
+/**
+ * Destroys the properties interface.
+ *
+ * \note Even when you are certain that you did not use the interface in a
+ * way that caused a memory allocation, you should call this function anyway.
+ * Future versions of the library might add features that need additional memory
+ * and you really don't want to search the entire code where you might need
+ * add call to this function.
+ *
+ * @param prop the properties interface
+ */
+__attribute__((__nonnull__))
+void cxPropertiesDestroy(CxProperties *prop);
 
 /**
  * Initialize a properties parser with the default configuration.
@@ -173,35 +214,72 @@
  * @param len the length of the data
  */
 __attribute__((__nonnull__))
-static inline void cxPropertiesInput(
+void cxPropertiesInput(
         CxProperties *prop,
-        const void *buf,
+        const char *buf,
         size_t len
-) {
-    prop->text = buf;
-    prop->text_len = len;
-    prop->text_pos = 0;
-}
+);
+
+/**
+ * Sets a new input buffer after copying the current unprocessed data
+ * to a temporary buffer.
+ *
+ * This temporary buffer is allocated on the heap, unless you specified
+ * a buffer on the stack with #cxPropertiesUseStack().
+ * In that case, the stack buffer is used, until the capacity is not sufficient
+ * anymore.
+ *
+ * When this function is called without any unprocessed data that needs to be
+ * copied, it behaves exactly as #cxPropertiesInput().
+ *
+ * @param prop the properties interface
+ * @param buf a pointer to data
+ * @param len the length of the data
+ * @return non-zero when a memory allocation was necessary but failed
+ */
+__attribute__((__nonnull__))
+int cxPropertiesFill(
+        CxProperties *prop,
+        const char *buf,
+        size_t len
+);
+
+/**
+ * Specifies stack memory that shall be used by #cxPropertiesFill().
+ *
+ * @param prop the properties interface
+ * @param buf a pointer to stack memory
+ * @param capacity the capacity of the stack memory
+ */
+void cxPropertiesUseStack(
+        CxProperties *prop,
+        char *buf,
+        size_t capacity
+);
 
 /**
  * Retrieves the next key/value-pair.
  *
- * This function returns zero as long as there are key/value-pairs
- * found. If no more key/value-pairs are found, #CX_PROPERTIES_NO_DATA
- * is returned. You may refill the input buffer with cxPropertiesInput().
+ * This function returns zero as long as there are key/value-pairs found.
+ * If no more key/value-pairs are found, #CX_PROPERTIES_NO_DATA is returned.
  *
- * When an invalid line is encountered, #CX_PROPERTIES_INVALID_LINE is returned
- * and the position of the input buffer will be the start of the affected line.
+ * When an incomplete line is encountered, #CX_PROPERTIES_INCOMPLETE_DATA is
+ * returned and the position of the input buffer will be the start of the
+ * affected line. You can then add more data with #cxPropertiesFill().
+ *
+ * \attention The returned strings will point into a buffer that might not be
+ * available later. It is strongly recommended to copy the strings for further
+ * use.
  *
  * @param prop the properties interface
- * @param name a pointer to the cxstring that shall contain the property name
+ * @param key a pointer to the cxstring that shall contain the property name
  * @param value a pointer to the cxstring that shall contain the property value
- * @return Nonzero, if a key/value-pair was successfully retrieved
- * @see ucx_properties_fill()
+ * @return the status code as defined above
+ * @see cxPropertiesFill()
  */
 enum cx_properties_status cxPropertiesNext(
         CxProperties *prop,
-        cxstring *name,
+        cxstring *key,
         cxstring *value
 );
 
--- a/src/properties.c	Thu Oct 10 18:40:27 2024 +0200
+++ b/src/properties.c	Sat Oct 12 19:34:19 2024 +0200
@@ -28,10 +28,259 @@
 
 #include "cx/properties.h"
 
+#include <string.h>
+#include <assert.h>
+
+static const int CX_PROPERTIES_FLAG_USE_STACK = 0x01;
+
 const CxPropertiesConfig cx_properties_config_default = {
         '=',
-        '\\',
+        //'\\',
         '#',
         '\0',
         '\0'
 };
+
+void cxPropertiesInit(
+        CxProperties *prop,
+        CxPropertiesConfig config
+) {
+    memset(prop, 0, sizeof(CxProperties));
+    prop->config = config;
+}
+
+void cxPropertiesDestroy(CxProperties *prop) {
+    if (0 == (prop->flags & CX_PROPERTIES_FLAG_USE_STACK)) {
+        free(prop->buf);
+    }
+    prop->buf = NULL;
+    prop->buf_capacity = prop->buf_size = 0;
+}
+
+static int cx_properties_ensure_buf_capacity(CxProperties *prop, size_t cap) {
+    if (prop->buf_capacity >= cap) {
+        return 0;
+    }
+
+    // not enough capacity - are we on the stack right now?
+    if ((prop->flags & CX_PROPERTIES_FLAG_USE_STACK) != 0) {
+        // move to the heap
+        char *newbuf = malloc(cap);
+        if (newbuf == NULL) return 1;
+        memcpy(newbuf, prop->buf, prop->buf_size);
+        prop->buf = newbuf;
+        prop->flags &= CX_PROPERTIES_FLAG_USE_STACK;
+    } else {
+        // we are on the heap already, reallocate
+        // this is legit, because realloc() behaves like malloc() when the
+        // current pointer is NULL
+        char *newbuf = realloc(prop->buf, cap);
+        if (newbuf == NULL) return 1;
+        prop->buf = newbuf;
+    }
+
+    // store new capacity and return
+    prop->buf_capacity = cap;
+    return 0;
+}
+
+static int cx_properties_rescuen_input(CxProperties *prop, size_t len) {
+    if (cx_properties_ensure_buf_capacity(prop, prop->buf_size + len)) {
+        return 1;
+    }
+    const char *src = prop->text + prop->text_pos;
+    char *dest = prop->buf + prop->buf_size;
+    memcpy(dest, src, len);
+    prop->buf_size += len;
+    return 0;
+}
+
+static int cx_properties_rescue_input(CxProperties *prop) {
+    // someone fucked around with our integers, exit immediately
+    if (prop->text_pos > prop->text_size) return 0;
+
+    // determine the bytes needed
+    size_t len = prop->text_size - prop->text_pos;
+
+    return cx_properties_rescuen_input(prop, len);
+}
+
+void cxPropertiesInput(
+        CxProperties *prop,
+        const char *buf,
+        size_t len
+) {
+    prop->text = buf;
+    prop->text_size = len;
+    prop->text_pos = 0;
+}
+
+int cxPropertiesFill(
+        CxProperties *prop,
+        const char *buf,
+        size_t len
+) {
+    if (cx_properties_rescue_input(prop)) return 1;
+    cxPropertiesInput(prop, buf, len);
+    return 0;
+}
+
+void cxPropertiesUseStack(
+        CxProperties *prop,
+        char *buf,
+        size_t capacity
+) {
+    assert(buf == NULL);
+    prop->buf = buf;
+    prop->buf_capacity = capacity;
+    prop->buf_size = 0;
+    prop->flags |= CX_PROPERTIES_FLAG_USE_STACK;
+}
+
+enum cx_properties_status cxPropertiesNext(
+        CxProperties *prop,
+        cxstring *key,
+        cxstring *value
+) {
+    // check if we have a text buffer
+    if (prop->text == NULL) {
+        return CX_PROPERTIES_NULL_INPUT;
+    }
+    // check if we have rescued data
+    if (prop->buf_size > 0) {
+        // check if we can now get a complete line
+        const char *buf = prop->text + prop->text_pos;
+        size_t len = prop->text_size - prop->text_pos;
+        cxstring str = cx_strn(buf, len);
+        cxstring nl = cx_strchr(str, '\n');
+        if(nl.length > 0) {
+            // we add as much data to the rescue buffer as we need
+            // to complete the line
+            size_t len_until_nl = (size_t)(nl.ptr - buf) + 1;
+
+            if (cx_properties_rescuen_input(prop, len_until_nl)) {
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+            }
+
+            // the tmp buffer contains exactly one line now
+            // we use a trick here: we swap the buffers and recurse
+            const char *orig_text = prop->text;
+            size_t orig_size = prop->text_size;
+            prop->text = prop->buf;
+            prop->text_size = prop->buf_size;
+            prop->text_pos = 0;
+            prop->buf_size = 0;
+
+            enum cx_properties_status result;
+            result = cxPropertiesNext(prop, key, value);
+
+            // restore original buffer
+            prop->text = orig_text;
+            prop->text_size = orig_size;
+
+            // set the position to after the newline
+            prop->text_pos = len_until_nl;
+
+            // check the result
+            if (result == CX_PROPERTIES_NO_ERROR) {
+                // reset the rescue buffer and return with the result
+                prop->buf_size = 0;
+                return result;
+            } else if (result == CX_PROPERTIES_NO_DATA) {
+                // rescue buffer contained only blanks or comments
+                // reset the rescue buffer and retry with text buffer
+                prop->buf_size = 0;
+                return cxPropertiesNext(prop, key, value);
+            } else {
+                // CX_PROPERTIES_INCOMPLETE_DATA is not possible
+                // so it must have been another error
+                // do not reset the rescue buffer and return the error
+                return result;
+            }
+        } else {
+            // still not enough data
+            return CX_PROPERTIES_INCOMPLETE_DATA;
+        }
+    }
+
+    char comment1 = prop->config.comment1;
+    char comment2 = prop->config.comment2;
+    char comment3 = prop->config.comment3;
+    char delimiter = prop->config.delimiter;
+
+    // get one line and parse it
+    while (prop->text_pos < prop->text_size) {
+        const char *buf = prop->text + prop->text_pos;
+        size_t len = prop->text_size - prop->text_pos;
+
+        /*
+         * First we check if we have at least one line. We also get indices of
+         * delimiter and comment chars
+         */
+        size_t delimiter_index = 0;
+        size_t comment_index = 0;
+        bool has_comment = false;
+
+        size_t i = 0;
+        char c = 0;
+        for (; i < len; i++) {
+            c = buf[i];
+            if (c == comment1 || c == comment2 || c == comment3) {
+                if (comment_index == 0) {
+                    comment_index = i;
+                    has_comment = true;
+                }
+            } else if (c == delimiter) {
+                if (delimiter_index == 0 && !has_comment) {
+                    delimiter_index = i;
+                }
+            } else if (c == '\n') {
+                break;
+            }
+        }
+
+        if (c != '\n') {
+            // we don't have enough data for a line
+            return CX_PROPERTIES_INCOMPLETE_DATA;
+        }
+
+        cxstring line = has_comment ?
+                        cx_strn(buf, comment_index) :
+                        cx_strn(buf, i);
+        // check line
+        if (delimiter_index == 0) {
+            // if line is not blank ...
+            line = cx_strtrim(line);
+            // ... either no delimiter found, or key is empty
+            if (line.length > 0) {
+                if (line.ptr[0] == delimiter) {
+                    return CX_PROPERTIES_INVALID_EMPTY_KEY;
+                } else {
+                    return CX_PROPERTIES_INVALID_MISSING_DELIMITER;
+                }
+            }
+        } else {
+            cxstring k = cx_strn(buf, delimiter_index);
+            cxstring val = cx_strn(
+                    buf + delimiter_index + 1,
+                    line.length - delimiter_index - 1);
+            k = cx_strtrim(k);
+            val = cx_strtrim(val);
+            if (k.length > 0) {
+                *key = k;
+                *value = val;
+                prop->text_pos += i + 1;
+                assert(prop->text_pos <= prop->text_size);
+                return CX_PROPERTIES_NO_ERROR;
+            } else {
+                return CX_PROPERTIES_INVALID_EMPTY_KEY;
+            }
+        }
+
+        prop->text_pos += i + 1;
+    }
+
+    // when we come to this point, all data must have been read
+    assert(prop->text_pos == prop->text_size);
+    return CX_PROPERTIES_NO_DATA;
+}
--- a/tests/Makefile	Thu Oct 10 18:40:27 2024 +0200
+++ b/tests/Makefile	Sat Oct 12 19:34:19 2024 +0200
@@ -29,7 +29,7 @@
 
 SRC = util_allocator.c test_utils.c test_hash_key.c test_allocator.c \
 	test_compare.c test_string.c test_buffer.c test_iterator.c \
-	test_list.c test_tree.c test_hash_map.c \
+	test_list.c test_tree.c test_hash_map.c test_properties.c \
 	test_printf.c test_mempool.c ucxtest.c
 
 OBJ_EXT=.o
@@ -104,6 +104,13 @@
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
+$(TEST_DIR)/test_properties$(OBJ_EXT): test_properties.c ../src/cx/test.h \
+ ../src/cx/properties.h ../src/cx/common.h ../src/cx/string.h \
+ ../src/cx/allocator.h ../src/cx/array_list.h ../src/cx/list.h \
+ ../src/cx/collection.h ../src/cx/iterator.h ../src/cx/compare.h
+	@echo "Compiling $<"
+	$(CC) -o $@ $(CFLAGS) -c $<
+
 $(TEST_DIR)/test_string$(OBJ_EXT): test_string.c ../src/cx/test.h \
  util_allocator.h ../src/cx/allocator.h ../src/cx/common.h \
  ../src/cx/string.h ../src/cx/allocator.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_properties.c	Sat Oct 12 19:34:19 2024 +0200
@@ -0,0 +1,380 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/test.h"
+
+#include "cx/properties.h"
+
+CX_TEST(test_cx_properties_init) {
+    CxProperties prop;
+    CX_TEST_DO {
+        cxPropertiesInitDefault(&prop);
+
+        CX_TEST_ASSERT(prop.config.delimiter == '=');
+        CX_TEST_ASSERT(prop.config.comment1 == '#');
+        CX_TEST_ASSERT(prop.config.comment2 == 0);
+        CX_TEST_ASSERT(prop.config.comment3 == 0);
+        CX_TEST_ASSERT(prop.flags == 0);
+        CX_TEST_ASSERT(prop.text == NULL);
+        CX_TEST_ASSERT(prop.buf == NULL);
+
+        cxPropertiesDestroy(&prop);
+    }
+}
+
+CX_TEST(test_cx_properties_next) {
+    const char *tests[] = {
+        "name = value\n",
+        "name=value\n",
+        "n=value\n",
+        "name=v\n",
+        "n=v\n",
+        "name = value # comment\n",
+        "#comment\nn=v\n",
+        "# comment1\n# comment2\n\n    \n\nname = value\n",
+        "    name     =      value\n",
+        "name = value\n\n"
+    };
+
+    const char *keys[] = {
+        "name",
+        "name",
+        "n",
+        "name",
+        "n",
+        "name",
+        "n",
+        "name",
+        "name",
+        "name"
+    };
+
+    const char *values[] = {
+        "value",
+        "value",
+        "value",
+        "v",
+        "v",
+        "value",
+        "v",
+        "value",
+        "value",
+        "value"
+    };
+
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+    enum cx_properties_status result;
+    cxstring key;
+    cxstring value;
+    CX_TEST_DO {
+        for (int i = 0; i < 10; i++) {
+            cxPropertiesInput(&prop, tests[i], strlen(tests[i]));
+            CX_TEST_ASSERT(prop.text == tests[i]);
+            CX_TEST_ASSERT(prop.text_size == strlen(tests[i]));
+            CX_TEST_ASSERT(prop.text_pos == 0);
+
+            result = cxPropertiesNext(&prop, &key, &value);
+            cxstring k = cx_str(keys[i]);
+            cxstring v = cx_str(values[i]);
+            CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+            CX_TEST_ASSERT(0 == cx_strcmp(key, k));
+            CX_TEST_ASSERT(0 == cx_strcmp(value, v));
+
+            result = cxPropertiesNext(&prop, &key, &value);
+            CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+        }
+    }
+    cxPropertiesDestroy(&prop);
+}
+
+CX_TEST(test_cx_properties_next_multi) {
+    const char *keys[] = {
+        "a",
+        "b",
+        "c",
+        "uap",
+        "name",
+        "key1",
+        "key2",
+        "key3"
+    };
+
+    const char *values[] = {
+        "a value",
+        "b value",
+        "core",
+        "core",
+        "ucx",
+        "value1",
+        "value2",
+        "value3"
+    };
+
+    const char *str = "#\n"
+        "# properties\n"
+        "# contains key/value pairs\n"
+        "#\n"
+        "a = a value\n"
+        "b = b value\n"
+        "c = core\n"
+        "\n# test\n"
+        "uap = core\n"
+        "name = ucx\n"
+        "# no = property\n"
+        "key1 = value1\n"
+        "#key1 = wrong value\n"
+        "#key2 = not value 2\n"
+        "key2 = value2\n"
+        "\n\n\n        \n           key3=value3\n";
+
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+    enum cx_properties_status result;
+    cxstring key;
+    cxstring value;
+
+    CX_TEST_DO {
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NULL_INPUT);
+        cxPropertiesInput(&prop, str, strlen(str));
+        for (int i = 0; i < 8; i++) {
+            result = cxPropertiesNext(&prop, &key, &value);
+            CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+            CX_TEST_ASSERT(!cx_strcmp(key, cx_str(keys[i])));
+            CX_TEST_ASSERT(!cx_strcmp(value, cx_str(values[i])));
+        }
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+    }
+    cxPropertiesDestroy(&prop);
+}
+
+CX_TEST(test_cx_properties_next_part) {
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+    enum cx_properties_status result;
+    cxstring key;
+    cxstring value;
+    const char *str;
+
+    CX_TEST_DO {
+        str = "";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+
+        str = "  \n";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+
+        str = "name";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = "    ";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        // call fill twice in a row
+        str = "= ";
+        cxPropertiesFill(&prop, str, strlen(str));
+        str = "value";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = "\n";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("name")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("value")));
+
+        // second round
+        str = "#comment\n";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+
+        str = "#comment\nname2 = ";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = "value2\na = b\n";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("name2")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("value2")));
+
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("a")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("b")));
+
+        str = "# comment\n#\n#\ntests = ";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = "test1 ";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = "test2 test3 test4\n";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("tests")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("test1 test2 test3 test4")));
+
+        // test if cxPropertiesNext finds a name/value after a comment
+        str = "# just a comment";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = " in 3";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = " parts\nx = 1\n";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("x")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("1")));
+        
+        // finally we are done
+        result = cxPropertiesNext(&prop, &key,  &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+    }
+    cxPropertiesDestroy(&prop);
+}
+
+CX_TEST(test_ucx_properties_next_long_lines) {
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+    enum cx_properties_status result;
+    cxstring key;
+    cxstring value;
+    
+    size_t key_len = 512;
+    char *long_key = (char*)malloc(key_len);
+    memset(long_key, 'a', 70);
+    memset(long_key + 70, 'b', 242);
+    memset(long_key + 312, 'c', 200);
+
+    size_t value_len = 2048;
+    char *long_value = (char*)malloc(value_len);
+    memset(long_value, 'x', 1024);
+    memset(long_value+1024, 'y', 1024);
+
+    CX_TEST_DO {
+        cxPropertiesFill(&prop, long_key, 10);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        cxPropertiesFill(&prop, long_key + 10, 202);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        cxPropertiesFill(&prop, long_key + 212, 200);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        cxPropertiesFill(&prop, long_key + 412, 100);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        const char *str = " = ";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        cxPropertiesFill(&prop, long_value, 512);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        cxPropertiesFill(&prop, long_value + 512, 1024);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        cxPropertiesFill(&prop, long_value + 1536, 512);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+
+        str = "\n#comment\nkey = value\n";
+        cxPropertiesFill(&prop, str, strlen(str));
+        result = cxPropertiesNext(&prop, &key, &value);
+        cxstring k = cx_strn(long_key, key_len);
+        cxstring v = cx_strn(long_value, value_len);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, k));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, v));
+
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("key")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("value")));
+
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+
+        CX_TEST_ASSERT(prop.buf != NULL);
+        CX_TEST_ASSERT(prop.buf_capacity > 0);
+        CX_TEST_ASSERT(prop.buf_size == 0);
+        cxPropertiesDestroy(&prop);
+
+        CX_TEST_ASSERT(prop.buf == NULL);
+        CX_TEST_ASSERT(prop.buf_capacity == 0);
+        CX_TEST_ASSERT(prop.buf_size == 0);
+    }
+
+    free(long_key);
+    free(long_value);
+}
+
+CxTestSuite *cx_test_suite_properties(void) {
+    CxTestSuite *suite = cx_test_suite_new("properties");
+
+    cx_test_register(suite, test_cx_properties_init);
+    cx_test_register(suite, test_cx_properties_next);
+    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);
+
+    return suite;
+}
--- a/tests/ucxtest.c	Thu Oct 10 18:40:27 2024 +0200
+++ b/tests/ucxtest.c	Sat Oct 12 19:34:19 2024 +0200
@@ -46,6 +46,7 @@
 CxTestSuite *cx_test_suite_tree_high_level(void);
 CxTestSuite *cx_test_suite_mempool(void);
 CxTestSuite *cx_test_suite_hash_map(void);
+CxTestSuite *cx_test_suite_properties(void);
 
 #define run_tests(suite) cx_test_run_stdout(suite); success += (suite)->success; failure += (suite)->failure
 #define execute_test_suites(...) unsigned success = 0, failure = 0; CxTestSuite* test_suites[] = {__VA_ARGS__}; \
@@ -73,7 +74,8 @@
             cx_test_suite_tree_low_level(),
             cx_test_suite_tree_high_level(),
             cx_test_suite_mempool(),
-            cx_test_suite_hash_map()
+            cx_test_suite_hash_map(),
+            cx_test_suite_properties()
     );
     printf("=== OVERALL RESULT ===\n");
     printf("  Total:   %u\n  Success: %u\n  Failure: %u\n",

mercurial