rework of properties parser - fixes #529 and resolves #458

Fri, 20 Dec 2024 15:00:31 +0100

author
Mike Becker <universe@uap-core.de>
date
Fri, 20 Dec 2024 15:00:31 +0100
changeset 1031
8a90552bba29
parent 1030
06091e067bee
child 1032
aaad28e23dac

rework of properties parser - fixes #529 and resolves #458

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
--- a/src/Makefile	Fri Dec 20 15:00:05 2024 +0100
+++ b/src/Makefile	Fri Dec 20 15:00:31 2024 +0100
@@ -133,7 +133,7 @@
 
 $(build_dir)/properties$(OBJ_EXT): properties.c cx/properties.h \
  cx/common.h cx/string.h cx/allocator.h cx/map.h cx/collection.h \
- cx/iterator.h cx/compare.h cx/hash_key.h cx/array_list.h cx/list.h
+ cx/iterator.h cx/compare.h cx/hash_key.h cx/buffer.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS)  -c $<
 
--- a/src/cx/properties.h	Fri Dec 20 15:00:05 2024 +0100
+++ b/src/cx/properties.h	Fri Dec 20 15:00:31 2024 +0100
@@ -39,7 +39,7 @@
 #include "common.h"
 #include "string.h"
 #include "map.h"
-#include "array_list.h"
+#include "buffer.h"
 
 #include <stdio.h>
 #include <string.h>
@@ -169,39 +169,14 @@
     CxPropertiesConfig config;
 
     /**
-     * The text buffer.
-     */
-    const char *text;
-
-    /**
-     * Size of the text buffer.
+     * The text input buffer.
      */
-    size_t text_size;
-
-    /**
-     * Position in the text buffer.
-     */
-    size_t text_pos;
+    CxBuffer input;
 
     /**
-     * Temporary internal buffer.
-     */
-    char *buf;
-
-    /**
-     * Size of the internal buffer.
+     * Internal buffer.
      */
-    size_t buf_size;
-
-    /**
-     * Capacity of the internal buffer.
-     */
-    size_t buf_capacity;
-
-    /**
-     * Internal flags.
-     */
-    int flags;
+    CxBuffer buffer;
 };
 
 /**
@@ -547,7 +522,7 @@
 #endif
 
 /**
- * Specifies stack memory that shall be used by #cxPropertiesFill().
+ * Specifies stack memory that shall be used as internal buffer.
  *
  * @param prop the properties interface
  * @param buf a pointer to stack memory
--- a/src/properties.c	Fri Dec 20 15:00:05 2024 +0100
+++ b/src/properties.c	Fri Dec 20 15:00:31 2024 +0100
@@ -30,8 +30,6 @@
 
 #include <assert.h>
 
-static const int CX_PROPERTIES_FLAG_USE_STACK = 0x01;
-
 const CxPropertiesConfig cx_properties_config_default = {
         '=',
         //'\\',
@@ -49,60 +47,8 @@
 }
 
 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;
-    prop->text_pos += 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);
+    cxBufferDestroy(&prop->input);
+    cxBufferDestroy(&prop->buffer);
 }
 
 int cxPropertiesFilln(
@@ -110,10 +56,15 @@
         const char *buf,
         size_t len
 ) {
-    if (cx_properties_rescue_input(prop)) return 1;
-    prop->text = buf;
-    prop->text_size = len;
-    prop->text_pos = 0;
+    if (cxBufferEof(&prop->input)) {
+        // destroy a possible previously initialized buffer
+        cxBufferDestroy(&prop->input);
+        cxBufferInit(&prop->input, (void*) buf, len,
+            NULL, CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_AUTO_EXTEND);
+        prop->input.size = len;
+    } else {
+        if (cxBufferAppend(buf, 1, len, &prop->input) < len) return -1;
+    }
     return 0;
 }
 
@@ -122,11 +73,7 @@
         char *buf,
         size_t capacity
 ) {
-    assert(prop->buf == NULL);
-    prop->buf = buf;
-    prop->buf_capacity = capacity;
-    prop->buf_size = 0;
-    prop->flags |= CX_PROPERTIES_FLAG_USE_STACK;
+    cxBufferInit(&prop->buffer, buf, capacity, NULL, CX_BUFFER_COPY_ON_EXTEND);
 }
 
 CxPropertiesStatus cxPropertiesNext(
@@ -135,65 +82,42 @@
         cxstring *value
 ) {
     // check if we have a text buffer
-    if (prop->text == NULL) {
+    if (prop->input.space == NULL) {
         return CX_PROPERTIES_NULL_INPUT;
     }
+
+    // a pointer to the buffer we want to read from
+    CxBuffer *current_buffer = &prop->input;
+
     // check if we have rescued data
-    if (prop->buf_size > 0) {
+    if (!cxBufferEof(&prop->buffer)) {
         // 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) {
+        cxstring input = cx_strn(prop->input.space + prop->input.pos,
+            prop->input.size - prop->input.pos);
+        cxstring nl = cx_strchr(input, '\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;
+            size_t len_until_nl = (size_t)(nl.ptr - input.ptr) + 1;
 
-            if (cx_properties_rescuen_input(prop, len_until_nl)) {
+            if (cxBufferAppend(input.ptr, 1,
+                len_until_nl, &prop->buffer) < 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;
-
-            CxPropertiesStatus 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;
+            // advance the position in the input buffer
+            prop->input.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;
-            }
+            // we now want to read from the rescue buffer
+            current_buffer = &prop->buffer;
         } else {
-            // still not enough data
-            if (cx_properties_rescue_input(prop)) {
+            // still not enough data, copy input buffer to internal buffer
+            if (cxBufferAppend(input.ptr, 1,
+                input.length, &prop->buffer) < input.length) {
                 return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
             }
+            // reset the input buffer (make way for a re-fill)
+            cxBufferReset(&prop->input);
             return CX_PROPERTIES_INCOMPLETE_DATA;
         }
     }
@@ -204,9 +128,9 @@
     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;
+    while (!cxBufferEof(current_buffer)) {
+        const char *buf = current_buffer->space + current_buffer->pos;
+        size_t len = current_buffer->size - current_buffer->pos;
 
         /*
          * First we check if we have at least one line. We also get indices of
@@ -235,10 +159,23 @@
         }
 
         if (c != '\n') {
-            // we don't have enough data for a line
-            if (cx_properties_rescue_input(prop)) {
+            // we don't have enough data for a line, use the rescue buffer
+            assert(current_buffer != &prop->buffer);
+            // make sure that the rescue buffer does not already contain something
+            assert(cxBufferEof(&prop->buffer));
+            if (prop->buffer.space == NULL) {
+                // initialize a rescue buffer, if the user did not provide one
+                cxBufferInit(&prop->buffer, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND);
+            } else {
+                // from a previous rescue there might be already read data
+                // reset the buffer to avoid unnecessary buffer extension
+                cxBufferReset(&prop->buffer);
+            }
+            if (cxBufferAppend(buf, 1, len, &prop->buffer) < len) {
                 return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
             }
+            // reset the input buffer (make way for a re-fill)
+            cxBufferReset(&prop->input);
             return CX_PROPERTIES_INCOMPLETE_DATA;
         }
 
@@ -256,6 +193,21 @@
                 } else {
                     return CX_PROPERTIES_INVALID_MISSING_DELIMITER;
                 }
+            } else {
+                // skip blank line
+                // if it was the rescue buffer, return to the original buffer
+                if (current_buffer == &prop->buffer) {
+                    // assert that the rescue buffer really does not contain more data
+                    assert(current_buffer->pos + i + 1 == current_buffer->size);
+                    // reset the rescue buffer, but don't destroy it!
+                    cxBufferReset(&prop->buffer);
+                    // continue with the input buffer
+                    current_buffer = &prop->input;
+                } else {
+                    // if it was the input buffer already, just advance the position
+                    current_buffer->pos += i + 1;
+                }
+                continue;
             }
         } else {
             cxstring k = cx_strn(buf, delimiter_index);
@@ -267,19 +219,21 @@
             if (k.length > 0) {
                 *key = k;
                 *value = val;
-                prop->text_pos += i + 1;
-                assert(prop->text_pos <= prop->text_size);
+                current_buffer->pos += i + 1;
+                assert(current_buffer->pos <= current_buffer->size);
                 return CX_PROPERTIES_NO_ERROR;
             } else {
                 return CX_PROPERTIES_INVALID_EMPTY_KEY;
             }
         }
-
-        prop->text_pos += i + 1;
+        // unreachable - either we returned or skipped a blank line
+        assert(false);
     }
 
     // when we come to this point, all data must have been read
-    assert(prop->text_pos == prop->text_size);
+    assert(cxBufferEof(&prop->buffer));
+    assert(cxBufferEof(&prop->input));
+
     return CX_PROPERTIES_NO_DATA;
 }
 
@@ -310,7 +264,7 @@
         CxPropertiesSource *src,
         cxstring *target
 ) {
-    if (prop->text == src->src) {
+    if (prop->input.space == src->src) {
         // when the input buffer already contains the string
         // we have nothing more to provide
         target->length = 0;
@@ -351,6 +305,7 @@
     CxPropertiesSource src;
     src.src = (void*) str.ptr;
     src.data_size = str.length;
+    src.data_ptr = NULL;
     src.read_func = cx_properties_read_string;
     src.read_init_func = NULL;
     src.read_clean_func = NULL;
@@ -361,6 +316,7 @@
     CxPropertiesSource src;
     src.src = (void*) str;
     src.data_size = len;
+    src.data_ptr = NULL;
     src.read_func = cx_properties_read_string;
     src.read_init_func = NULL;
     src.read_clean_func = NULL;
@@ -371,6 +327,7 @@
     CxPropertiesSource src;
     src.src = (void*) str;
     src.data_size = strlen(str);
+    src.data_ptr = NULL;
     src.read_func = cx_properties_read_string;
     src.read_init_func = NULL;
     src.read_clean_func = NULL;
@@ -381,6 +338,7 @@
     CxPropertiesSource src;
     src.src = file;
     src.data_size = chunk_size;
+    src.data_ptr = NULL;
     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;
@@ -420,9 +378,7 @@
         }
 
         // set the input buffer and read the k/v-pairs
-        prop->text = input.ptr;
-        prop->text_size = input.length;
-        prop->text_pos = 0;
+        cxPropertiesFill(prop, input);
 
         CxPropertiesStatus kv_status;
         do {
--- a/tests/Makefile	Fri Dec 20 15:00:05 2024 +0100
+++ b/tests/Makefile	Fri Dec 20 15:00:31 2024 +0100
@@ -121,8 +121,8 @@
  ../src/cx/common.h util_allocator.h ../src/cx/allocator.h \
  ../src/cx/properties.h ../src/cx/string.h ../src/cx/allocator.h \
  ../src/cx/map.h ../src/cx/collection.h ../src/cx/iterator.h \
- ../src/cx/compare.h ../src/cx/hash_key.h ../src/cx/array_list.h \
- ../src/cx/list.h ../src/cx/hash_map.h
+ ../src/cx/compare.h ../src/cx/hash_key.h ../src/cx/buffer.h \
+ ../src/cx/hash_map.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -I../src -c $<
 
--- a/tests/test_properties.c	Fri Dec 20 15:00:05 2024 +0100
+++ b/tests/test_properties.c	Fri Dec 20 15:00:31 2024 +0100
@@ -41,9 +41,8 @@
         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);
+        CX_TEST_ASSERT(prop.input.space == NULL);
+        CX_TEST_ASSERT(prop.buffer.space == NULL);
 
         cxPropertiesDestroy(&prop);
     }
@@ -97,9 +96,9 @@
     CX_TEST_DO {
         for (int i = 0; i < 10; i++) {
             cxPropertiesFill(&prop, 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);
+            CX_TEST_ASSERT(prop.input.space == tests[i]);
+            CX_TEST_ASSERT(prop.input.size == strlen(tests[i]));
+            CX_TEST_ASSERT(prop.input.pos == 0);
 
             result = cxPropertiesNext(&prop, &key, &value);
             cxstring k = cx_str(keys[i]);
@@ -203,35 +202,35 @@
 
     CX_TEST_DO {
         str = "";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
 
         str = "  \n";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
 
         str = "name";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         str = "    ";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         // call fill twice in a row
         str = "= ";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         str = "value";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         str = "\n";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
         CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("name")));
@@ -239,17 +238,17 @@
 
         // second round
         str = "#comment\n";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
 
         str = "#comment\nname2 = ";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         str = "value2\na = b\n";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
         CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("name2")));
@@ -261,17 +260,17 @@
         CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("b")));
 
         str = "# comment\n#\n#\ntests = ";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         str = "test1 ";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         str = "test2 test3 test4\n";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
         CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("tests")));
@@ -279,22 +278,22 @@
 
         // test if cxPropertiesNext finds a name/value after a comment
         str = "# just a comment";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         str = " in 3";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, str));
         result = cxPropertiesNext(&prop, &key,  &value);
         CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
 
         str = " parts\nx = 1\n";
-        cxPropertiesFill(&prop, str);
+        CX_TEST_ASSERT(0 == cxPropertiesFill(&prop, 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);
@@ -308,7 +307,7 @@
     CxPropertiesStatus result;
     cxstring key;
     cxstring value;
-    
+
     size_t key_len = 512;
     char *long_key = (char*)malloc(key_len);
     memset(long_key, 'a', 70);
@@ -371,14 +370,13 @@
         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);
+        CX_TEST_ASSERT(prop.buffer.capacity > 0);
+        CX_TEST_ASSERT(cxBufferEof(&prop.buffer));
+        CX_TEST_ASSERT(cxBufferEof(&prop.input));
         cxPropertiesDestroy(&prop);
-
-        CX_TEST_ASSERT(prop.buf == NULL);
-        CX_TEST_ASSERT(prop.buf_capacity == 0);
-        CX_TEST_ASSERT(prop.buf_size == 0);
+        CX_TEST_ASSERT(prop.buffer.capacity == 0);
+        CX_TEST_ASSERT(prop.buffer.size == 0);
+        CX_TEST_ASSERT(prop.buffer.pos == 0);
     }
 
     free(long_key);
@@ -521,6 +519,105 @@
     cx_testing_allocator_destroy(&talloc);
 }
 
+CX_TEST(test_properties_multiple_fill) {
+    const char *props1 = "key1 = value1\n";
+    const char *props2 = "key2 = value2\n";
+    const char *props3 = "key3 = value3\n";
+
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+    CxPropertiesStatus result;
+    cxstring key;
+    cxstring value;
+    CX_TEST_DO {
+        cxPropertiesFill(&prop, props1);
+        cxPropertiesFill(&prop, props2);
+        cxPropertiesFill(&prop, props3);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("key1")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("value1")));
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("key2")));
+        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("key3")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("value3")));
+
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+    }
+    cxPropertiesDestroy(&prop);
+}
+
+CX_TEST(test_properties_use_stack) {
+    const char *props1 = "key1 = val";
+    const char *props2 = "ue1\nkey2 = value2";
+    const char *props3 = "\nkey3 = value3\n";
+    char stackmem[16];
+
+    CxProperties prop;
+    cxPropertiesInitDefault(&prop);
+    cxPropertiesUseStack(&prop, stackmem, 16);
+    CxPropertiesStatus result;
+    cxstring key;
+    cxstring value;
+    CX_TEST_DO {
+        cxPropertiesFill(&prop, props1);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+        cxPropertiesFill(&prop, props2);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("key1")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("value1")));
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INCOMPLETE_DATA);
+        cxPropertiesFill(&prop, props3);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_ERROR);
+        CX_TEST_ASSERT(0 == cx_strcmp(key, cx_str("key2")));
+        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("key3")));
+        CX_TEST_ASSERT(0 == cx_strcmp(value, cx_str("value3")));
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_NO_DATA);
+    }
+    cxPropertiesDestroy(&prop);
+}
+
+CX_TEST(test_properties_empty_key) {
+    const char *fail1 = "= val\n";
+    const char *fail2 = "   = val\n";
+    const char *good = "  key = val\n";
+
+    CxProperties prop;
+    CxPropertiesStatus result;
+    cxstring key;
+    cxstring value;
+    CX_TEST_DO {
+        cxPropertiesInitDefault(&prop);
+        cxPropertiesFill(&prop, fail1);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INVALID_EMPTY_KEY);
+        cxPropertiesReset(&prop);
+        cxPropertiesFill(&prop, fail2);
+        result = cxPropertiesNext(&prop, &key, &value);
+        CX_TEST_ASSERT(result == CX_PROPERTIES_INVALID_EMPTY_KEY);
+        cxPropertiesReset(&prop);
+        cxPropertiesFill(&prop, good);
+        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("val")));
+        cxPropertiesDestroy(&prop);
+    }
+}
+
 CxTestSuite *cx_test_suite_properties(void) {
     CxTestSuite *suite = cx_test_suite_new("properties");
 
@@ -531,6 +628,9 @@
     cx_test_register(suite, test_properties_next_long_lines);
     cx_test_register(suite, test_properties_load_string_to_map);
     cx_test_register(suite, test_properties_load_file_to_map);
+    cx_test_register(suite, test_properties_multiple_fill);
+    cx_test_register(suite, test_properties_use_stack);
+    cx_test_register(suite, test_properties_empty_key);
 
     return suite;
 }

mercurial