# HG changeset patch # User Mike Becker # Date 1728754459 -7200 # Node ID 3c90dfc35f06b4785df7ec2bca08b3263d523a93 # Parent 45da884269c878bcd4b1bdad922134aa3c0eba6e add implementation for the properties parser relates to #429 diff -r 45da884269c8 -r 3c90dfc35f06 src/Makefile --- 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 $< diff -r 45da884269c8 -r 3c90dfc35f06 src/cx/properties.h --- 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 ); diff -r 45da884269c8 -r 3c90dfc35f06 src/properties.c --- 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 +#include + +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; +} diff -r 45da884269c8 -r 3c90dfc35f06 tests/Makefile --- 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 diff -r 45da884269c8 -r 3c90dfc35f06 tests/test_properties.c --- /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; +} diff -r 45da884269c8 -r 3c90dfc35f06 tests/ucxtest.c --- 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",