properties.h: removes the source/sink API and adds a new cxPropertiesLoad()

Sun, 07 Dec 2025 15:34:46 +0100

author
Mike Becker <universe@uap-core.de>
date
Sun, 07 Dec 2025 15:34:46 +0100
changeset 1555
8972247f54e8
parent 1554
91fb10c89611
child 1556
afdaa70034f8

properties.h: removes the source/sink API and adds a new cxPropertiesLoad()

resolves #610

CHANGELOG file | annotate | diff | comparison | revisions
docs/Writerside/topics/about.md file | annotate | diff | comparison | revisions
docs/Writerside/topics/install.md file | annotate | diff | comparison | revisions
docs/Writerside/topics/properties.h.md file | annotate | diff | comparison | revisions
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/CHANGELOG	Sun Dec 07 15:33:16 2025 +0100
+++ b/CHANGELOG	Sun Dec 07 15:34:46 2025 +0100
@@ -2,16 +2,19 @@
 ------------------------
 
  * adds cx_system_page_size() to allocator.h
+ * changes cxFreeDefault() from a macro to a function so that it can be used as a simple destructor
  * changes cxBufferReserve() to allow reducing the capacity
  * changes the members of CxJson and CxJsonValue
  * changes the return value of cxJsonObjIter() to CxMapIterator
  * changes CxTree structure so that it now inherits CX_COLLECTION_BASE
+ * changes cxPropertiesLoad() to directly load properties from a file to a CxMap
  * fixes cxJsonWrite() incorrectly returning non-zero when strings needed to be escaped
  * fixes critical memory leak when using cxMapFree() on a kv-list that is using destructors
  * fixes that overwriting items with cxMapPut() in a kv-list did not work
  * fixes that cxReallocate(), cxReallocateArray(), cx_reallocate(), and cx_reallocatearray()
    were not returning zero after freeing the memory when passed a size of zero
  * removes the sort_members feature from CxJsonWriter
+ * removes the source and sink API from properties.h
 
 Version 3.2 - 2025-11-30
 ------------------------
--- a/docs/Writerside/topics/about.md	Sun Dec 07 15:33:16 2025 +0100
+++ b/docs/Writerside/topics/about.md	Sun Dec 07 15:34:46 2025 +0100
@@ -29,16 +29,19 @@
 ### Version 4.0 - preview {collapsible="true"}
 
 * adds cx_system_page_size() to allocator.h
+* changes cxFreeDefault() from a macro to a function so that it can be used as a simple destructor
 * changes cxBufferReserve() to allow reducing the capacity
 * changes the members of CxJson and CxJsonValue
 * changes the return value of cxJsonObjIter() to CxMapIterator
 * changes CxTree structure so that it now inherits CX_COLLECTION_BASE
+* changes cxPropertiesLoad() to directly load properties from a file to a CxMap
 * fixes cxJsonWrite() incorrectly returning non-zero when strings needed to be escaped
 * fixes critical memory leak when using cxMapFree() on a kv-list that is using destructors
 * fixes that overwriting items with cxMapPut() in a kv-list did not work
 * fixes that cxReallocate(), cxReallocateArray(), cx_reallocate(), and cx_reallocatearray()
   were not returning zero after freeing the memory when passed a size of zero
 * removes the sort_members feature from CxJsonWriter
+* removes the source and sink API from properties.h
 
 ### Version 3.2 - 2025-11-30 {collapsible="true"}
 
--- a/docs/Writerside/topics/install.md	Sun Dec 07 15:33:16 2025 +0100
+++ b/docs/Writerside/topics/install.md	Sun Dec 07 15:34:46 2025 +0100
@@ -160,4 +160,14 @@
     <td>The buffer size on the heap for a stream copy.</td>
     <td>8,192</td>
 </tr>
+<tr>
+    <td>CX_PROPERTIES_LOAD_FILL_SIZE</td>
+    <td>The size of the stack buffer used to fill the parser in cxPropertiesLoad().</td>
+    <td>1,024</td>
+</tr>
+<tr>
+    <td>CX_PROPERTIES_LOAD_BUF_SIZE</td>
+    <td>The size of the stack used for the line buffer in cxPropertiesLoad().</td>
+    <td>256</td>
+</tr>
 </table>
--- a/docs/Writerside/topics/properties.h.md	Sun Dec 07 15:33:16 2025 +0100
+++ b/docs/Writerside/topics/properties.h.md	Sun Dec 07 15:34:46 2025 +0100
@@ -61,9 +61,15 @@
 
 CxPropertiesStatus cxPropertiesNext(CxProperties *prop,
         cxstring *key, cxstring *value);
-        
+
 void cxPropertiesUseStack(CxProperties *prop,
         char *buf, size_t capacity);
+
+CxPropertiesStatus cxPropertiesLoad(CxPropertiesConfig config,
+        AnyStr filename, CxMap *target);
+
+CxPropertiesStatus cxPropertiesLoadDefault(
+        AnyStr filename, CxMap *target);
 ```
 
 The first step is to initialize a `CxProperties` structure with a call to `cxPropertiesInit()` using the desired config.
@@ -105,242 +111,32 @@
 > It is strongly recommended to always call `cxPropertiesDestroy()` when you are done with the parser,
 > even if you did not expect any allocations because you used `cxPropertiesUseStack()`.
 
+All the above operations are combined in the function `cxPropertiesLoad()`,
+which opens the file designated by the `filename` and loads all properties from that file into the specified `CxMap`.
+The convenience macro `cxPropertiesLoadDefault()` uses the default parser configuration for this.
+The target map must either store pointers of type `char*` or elements of type `cxmutstr`.
+
+> The stack buffers used by `cxPropertiesLoad()` can be changed when building UCX from sources
+> by setting the `CX_PROPERTIES_LOAD_FILL_SIZE` and `CX_PROPERTIES_LOAD_BUF_SIZE` macros
+> (see [](install.md#small-buffer-optimizations)).
+
 ### List of Status Codes
 
-Below is a full list of status codes for `cxPropertiesNext()`.
+Below is a full list of status codes for `cxPropertiesNext()` and `cxPropertiesLoad()`.
 
 | Status Code                             | Meaning                                                                                                                                                                                                                 |
 |-----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | CX_PROPERTIES_NO_ERROR                  | A key/value pair was found and returned.                                                                                                                                                                                |
-| CX_PROPERTIES_NO_DATA                   | The input buffer does not contain more data.                                                                                                                                                                            |
+| CX_PROPERTIES_NO_DATA                   | The input buffer does not contain data.                                                                                                                                                                                 |
 | CX_PROPERTIES_INCOMPLETE_DATA           | The input ends unexpectedly. This can happen when the last line does not terminate with a line break, or when the input ends with a parsed key but no value. Use `cxPropertiesFill()` to add more data before retrying. |
 | CX_PROPERTIES_NULL_INPUT                | The input buffer was never initialized. Probably you forgot to call `cxPropertiesFill()` at least once.                                                                                                                 |
 | CX_PROPERTIES_INVALID_EMPTY_KEY         | Only white-spaces were found on the left hand-side of the delimiter. Keys must not be empty.                                                                                                                            |
 | CX_PROPERTIES_INVALID_MISSING_DELIMITER | A line contains data, but no delimiter.                                                                                                                                                                                 |
 | CX_PROPERTIES_BUFFER_ALLOC_FAILED       | More internal buffer was needed, but could not be allocated.                                                                                                                                                            |
-
-
-## Sources and Sinks
-
-```C
-#include <cx/properties.h>
-
-CxPropertiesSource
-cxPropertiesStringSource(cxstring str);
-
-CxPropertiesSource
-cxPropertiesCstrSource(const char *str);
-
-CxPropertiesSource
-cxPropertiesCstrnSource(const char *str, size_t len);
-
-CxPropertiesSource
-cxPropertiesFileSource(FILE *file, size_t chunk_size);
-        
-CxPropertiesSink
-cxPropertiesMapSink(CxMap *map);
-
-CxPropertiesStatus
-cxPropertiesLoad(CxProperties *prop,
-        CxPropertiesSink sink, CxPropertiesSource source);
-```
-
-The basic idea of `cxPropertiesLoad()` is that key/value-pairs are extracted from a _source_ and ingested by a _sink_.
-For the most common scenarios where properties data is read from a string or a file and put into a map, several functions are available.
-But you can specify your [own sources and sinks](#creating-own-sources-and-sinks), as well.
-
-The following example shows a simple function which loads all properties data from a file.
-The `chunk_size` argument when creating the file source specifies
-how many bytes are read from the file and filled into the properties parser in one read/sink cycle.
-
-```C
-#include <stdio.h>
-#include <cx/properties.h>
-
-int load_props_from_file(const char *filename, CxMap *map) {
-    FILE *f = fopen(filename, "r");
-    if (!f) return -1;
-    CxProperties prop;
-    cxPropertiesInitDefault(&prop);
-    CxPropertiesSink sink = cxPropertiesMapSink(map);
-    CxPropertiesSource src = cxPropertiesFileSource(f, 512);
-    CxPropertiesStatus status = cxPropertiesLoad(&prop, sink, src);
-    fclose(f);
-    return status;
-}
-
-// usage:
-CxMap *map = cxHashMapCreateSimple(CX_STORE_POINTERS);
-if (load_props_from_file("my-props.properties", map)) {
-    // error handling
-} else {
-    // assuming my-props.properties contains the following line:
-    // my-key = some value
-    char *value = cxMapGet(map, "my-key");
-}
-```
-
-> The function `cxPropertiesLoad()` should usually not return `CX_PROPERTIES_INCOMPLETE_DATA` because the parser is automatically refilled from the source.
-> If it does, it could mean that the source was unable to provide all the data, or the properties data ended unexpectedly.
-> The most expected status code is `CX_PROPERTIES_NO_ERROR` which means that at least one key/value-pair was found.
-> If `cxPropertiesLoad()` returns `CX_PROPERTIES_NO_DATA` it means that the source did not provide any key/value-pair.
-> There are several special status codes that are documented [below](#additional-status-codes). 
-
-### Creating own Sources and Sinks
-
-```C
-#include <cx/properties.h>
-
-typedef int(*cx_properties_read_init_func)(CxProperties *prop,
-        CxPropertiesSource *src);
-
-typedef int(*cx_properties_read_func)(CxProperties *prop,
-        CxPropertiesSource *src, cxstring *target);
-
-typedef void(*cx_properties_read_clean_func)(CxProperties *prop,
-        CxPropertiesSource *src);
-
-typedef int(*cx_properties_sink_func)(CxProperties *prop,
-        CxPropertiesSink *sink, cxstring key, cxstring value);
-
-typedef struct cx_properties_source_s {
-    void *src;
-    void *data_ptr;
-    size_t data_size;
-    cx_properties_read_func read_func;
-    cx_properties_read_init_func read_init_func;
-    cx_properties_read_clean_func read_clean_func;
-} CxPropertiesSource;
-
-typedef struct cx_properties_sink_s {
-    void *sink;
-    void *data;
-    cx_properties_sink_func sink_func;
-} CxPropertiesSink;
-```
-
-You can create your own sources and sinks by initializing the respective structures.
-
-For a source, only the `read_func` is mandatory, the other two functions are optional and used for initialization and cleanup, if required.
-The file source created by `cxPropertiesFileSource()`, for example,
-uses the `read_init_func` to allocate, and the `read_clean_func` to free the read buffer, respectively. 
-
-Since the default map sink created by `cxPropertiesMapSink()` stores `char*` pointers into a map,
-the following example uses a different sink, which stores them as `cxmutstr` values, automatically freeing them
-when the map gets destroyed.
-And instead of reading the data from a file with `fread()`, it uses `mmap()` to map the file into memory for reading.
+| CX_PROPERTIES_FILE_ERROR                | A file operation failed (only for `cxPropertiesLoad()`).                                                                                                                                                                |
 
-```C
-#include <stdio.h> 
-#include <unistd.h>
-#include <fcntl.h>
-#include <sys/stat.h>
-#include <sys/mman.h>
-#include <cx/properties.h>
-#include <cx/hash_map.h>
-
-static int prop_mmap(CxProperties *prop, CxPropertiesSource *src) {
-    struct stat s;
-    int fd = open(src->src, O_RDONLY);
-    if (fd < 0) return -1;
-    // re-use the data field to store the fd
-    // there are cleaner ways, but this is just for illustration
-    src->src = (void*) fd;
-    fstat(fd, &s);
-    // memory map the entire file
-    // and store the address and length in the properties source
-    src->data_ptr = mmap(0, s.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
-    src->data_size = s.st_size;
-    return src->data_ptr == NULL;
-}
-
-static int prop_read(CxProperties *prop, CxPropertiesSource *src,
-        cxstring *target) {
-    // copy the address and length of the mapped data to the target 
-    target->ptr = src->data_ptr;
-    target->length = src->data_size;
-    // set the new size to zero to indicate that there is no more data
-    src->data_size = 0;
-    return 0;
-}
-
-static void prop_unmap(CxProperties *prop, CxPropertiesSource *src) {
-    // unmap the memory and close the file
-    munmap(src->data_ptr, src->data_size);
-    close((int)src->src);
-}
-
-static int prop_sink(CxProperties *prop, CxPropertiesSink *sink,
-        cxstring key, cxstring value) {
-    CxMap *map = sink->sink;
-    // copy the string and store it into the map
-    cxmutstr v = cx_strdup(value);
-    int r = cxMapPut(map, key, &v);
-    if (r != 0) cx_strfree(&v);
-    return r;
-}
-
-int load_props_from_file(const char *filename, CxMap *map) {
-    CxProperties prop;
-    cxPropertiesInitDefault(&prop);
-    CxPropertiesSource src;
-    src.src = (void*) filename;
-    src.read_init_func = prop_mmap;
-    src.read_func = prop_read;
-    src.read_clean_func = prop_unmap;
-    CxPropertiesSink sink;
-    sink.sink = map;
-    sink.sink_func = prop_sink;
-    return cxPropertiesLoad(&prop, sink, src);
-}
-
-int main() {
-    // in contrast to the default map sink,
-    // this one here stores the UCX strings by value
-    CxMap *map = cxHashMapCreateSimple(sizeof(cxmutstr));
-    
-    // automatically free the UCX string when removed from the map
-    cxDefineDestructor(map, cx_strfree);
-
-    // use our custom load function to load the data from the file
-    if (load_props_from_file("my-props.properties", map)) {
-        fputs("Error reading properties.\n", stderr);
-        return 1;
-    }
-
-    // output the read key/value pairs for illustration
-    CxMapIterator iter = cxMapIterator(map);
-    cx_foreach(CxMapEntry *, entry, iter) {
-        cxstring k = cx_strn(entry->key->data, entry->key->len);
-        cxmutstr *v = entry->value;
-        printf("%" CX_PRIstr " = %" CX_PRIstr "\n",
-            CX_SFMT(k), CX_SFMT(*v));
-    }
-
-    // freeing the map also frees the strings
-    // because we have registered cx_strfree() as destructor function
-    cxMapFree(map);
-
-    return 0;
-}
-```
-
-> A cleaner implementation that does not produce a warning for bluntly casting an `int` to a `void*`
-> can be achieved by declaring a struct that contains the information, allocate memory for
-> that struct, and store the pointer in `data_ptr`.
-> For illustrating how properties sources and sinks can be implemented, this was not necessary.
-
-### Additional Status Codes
-
-For sources and sinks there are three additional special status codes,
-which only appear as return values for `cxPropertiesLoad()`.
-
-| Status Code                             | Meaning                                                                                                                                                                                                                 |
-|-----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| CX_PROPERTIES_READ_INIT_FAILED          | Initializing the properties source failed and the `cx_properties_read_init_func` returned non-zero.                                                                                                                     |
-| CX_PROPERTIES_READ_FAILED               | Reading from a properties source failed and the `cx_properties_read_func` returned non-zero.                                                                                                                            |
-| CX_PROPERTIES_SINK_FAILED               | Sinking a key/value-pair failed and the `cx_properties_sink_func` returned non-zero.                                                                                                                                    |
-
+For `cxPropertiesLoad()` the status code `CX_PROPERTIES_NO_ERROR` means that at least one property was loaded into the map,
+while `CX_PROPERTIES_NO_DATA` means that the file is syntactically fine but does not contain any properties.
 
 <seealso>
 <category ref="apidoc">
--- a/src/Makefile	Sun Dec 07 15:33:16 2025 +0100
+++ b/src/Makefile	Sun Dec 07 15:34:46 2025 +0100
@@ -104,7 +104,7 @@
 
 $(build_dir)/json$(OBJ_EXT): json.c cx/json.h cx/common.h cx/allocator.h \
  cx/string.h cx/buffer.h cx/array_list.h cx/list.h cx/collection.h \
- cx/iterator.h cx/compare.h
+ cx/iterator.h cx/compare.h cx/map.h cx/hash_key.h cx/kv_list.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS)  -c $<
 
--- a/src/cx/properties.h	Sun Dec 07 15:33:16 2025 +0100
+++ b/src/cx/properties.h	Sun Dec 07 15:34:46 2025 +0100
@@ -41,9 +41,6 @@
 #include "map.h"
 #include "buffer.h"
 
-#include <stdio.h>
-#include <string.h>
-
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -141,23 +138,16 @@
      */
     CX_PROPERTIES_BUFFER_ALLOC_FAILED,
     /**
-     * Initializing the properties source failed.
-     *
-     * @see cx_properties_read_init_func
+     * A file operation failed.
+     * Only for cxPropertiesLoad().
+     * It is system-specific if errno is set.
      */
-    CX_PROPERTIES_READ_INIT_FAILED,
+    CX_PROPERTIES_FILE_ERROR,
     /**
-     * Reading from a properties source failed.
-     *
-     * @see cx_properties_read_func
+     * A map operation failed.
+     * Only for cxPropertiesLoad().
      */
-    CX_PROPERTIES_READ_FAILED,
-    /**
-     * Sinking a k/v-pair failed.
-     *
-     * @see cx_properties_sink_func
-     */
-    CX_PROPERTIES_SINK_FAILED,
+    CX_PROPERTIES_MAP_ERROR,
 };
 
 /**
@@ -190,134 +180,6 @@
  */
 typedef struct cx_properties_s CxProperties;
 
-
-/**
- * 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 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
- * @retval zero success
- * @retval non-zero sinking the k/v-pair failed
- */
-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
- * @retval zero success
- * @retval non-zero reading the data failed
- */
-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
- * @retval zero initialization was successful
- * @retval non-zero otherwise
- */
-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
- */
-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.
  *
@@ -465,100 +327,72 @@
 CX_EXPORT 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 freshly allocated,
- * zero-terminated C strings (@c char*), which means the @p map should have been
- * created with #CX_STORE_POINTERS.
- *
- * The cxDefaultAllocator will be used unless you specify a custom
- * allocator in the optional @c data field of the returned sink.
- *
- * @param map the map that shall consume the k/v-pairs.
- * @return the sink
- * @see cxPropertiesLoad()
+ * The size of the stack memory that `cxPropertiesLoad()` will reserve with `cxPropertiesUseStack()`.
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxPropertiesSink cxPropertiesMapSink(CxMap *map);
+CX_EXPORT extern const unsigned cx_properties_load_buf_size;
 
 /**
- * Creates a properties source based on an UCX string.
- *
- * @param str the string
- * @return the properties source
- * @see cxPropertiesLoad()
+ * The size of the stack memory that `cxPropertiesLoad()` will use to read contents from the file.
  */
-cx_attr_nodiscard
-CX_EXPORT 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()
- */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1, 2)
-CX_EXPORT CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
+CX_EXPORT extern const unsigned cx_properties_load_fill_size;
 
 /**
- * Creates a properties source based on a C string.
- *
- * The length will be determined with strlen(), so the string MUST be
- * zero-terminated.
+ * Internal function - use cxPropertiesLoad() instead.
  *
- * @param str the string
- * @return the properties source
- * @see cxPropertiesLoad()
+ * @param config the parser config
+ * @param filename the file name
+ * @param target the target map
+ * @return status code
  */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_cstr_arg(1)
-CX_EXPORT CxPropertiesSource cxPropertiesCstrSource(const char *str);
+cx_attr_nonnull
+CX_EXPORT CxPropertiesStatus cx_properties_load(CxPropertiesConfig config,
+        cxstring filename, CxMap *target);
 
 /**
- * Creates a properties source based on an FILE.
- *
- * @param file the file
- * @param chunk_size how many bytes may be read in one operation
+ * Loads properties from a file and inserts them into a map.
  *
- * @return the properties source
- * @see cxPropertiesLoad()
- */
-cx_attr_nonnull cx_attr_nodiscard cx_attr_access_r(1)
-CX_EXPORT CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
-
-
-/**
- * Loads properties data from a source and transfers it to a sink.
+ * Entries are added to the map, possibly overwriting existing entries.
+ *
+ * The map must either store pointers of type @c char*, or elements of type cxmutstr.
+ * Any other configuration is not supported.
  *
- * 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.
- * In case the source data ends unexpectedly, the #CX_PROPERTIES_INCOMPLETE_DATA
- * is returned. In that case you should call this function again with the same
- * sink and either an updated source or the same source if the source is able to
- * yield the missing data.
- *
- * The other result codes apply, according to their description.
- *
- * @param prop the properties interface
- * @param sink the sink
- * @param source the source
- * @retval CX_PROPERTIES_NO_ERROR (zero) a key/value pair was found
- * @retval CX_PROPERTIES_READ_INIT_FAILED initializing the source failed
- * @retval CX_PROPERTIES_READ_FAILED reading from the source failed
- * @retval CX_PROPERTIES_SINK_FAILED sinking the properties into the sink failed
- * @retval CX_PROPERTIES_NO_DATA the source did not provide any key/value pairs
- * @retval CX_PROPERTIES_INCOMPLETE_DATA the source did not provide enough data
+ * @param config the parser config
+ * @param filename (any string) the absolute or relative path to the file
+ * @param target (@c CxMap*) the map where the properties shall be added
+ * @retval CX_PROPERTIES_NO_ERROR (zero) at least one key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA the file is syntactically OK, but does not contain properties
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA unexpected end of file
  * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
  * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
  * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
+ * @retval CX_PROPERTIES_FILE_ERROR a file operation failed; depending on the system @c errno might be set
+ * @retval CX_PROPERTIES_MAP_ERROR storing a key/value pair in the map failed
+ * @see cxPropertiesLoadDefault()
  */
-cx_attr_nonnull
-CX_EXPORT CxPropertiesStatus cxPropertiesLoad(CxProperties *prop,
-        CxPropertiesSink sink, CxPropertiesSource source);
+#define cxPropertiesLoad(config, filename, target) cx_properties_load(config, cx_strcast(filename), target)
+
+/**
+ * Loads properties from a file and inserts them into a map with a default config.
+ *
+ * Entries are added to the map, possibly overwriting existing entries.
+ *
+ * The map must either store pointers of type @c char*, or elements of type cxmutstr.
+ * Any other configuration is not supported.
+ *
+ * @param filename (any string) the absolute or relative path to the file
+ * @param target (@c CxMap*) the map where the properties shall be added
+ * @retval CX_PROPERTIES_NO_ERROR (zero) at least one key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA the file is syntactically OK, but does not contain properties
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA unexpected end of file
+ * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
+ * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
+ * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
+ * @retval CX_PROPERTIES_FILE_ERROR a file operation failed; depending on the system @c errno might be set
+ * @retval CX_PROPERTIES_MAP_ERROR storing a key/value pair in the map failed
+ * @see cxPropertiesLoad()
+ */
+#define cxPropertiesLoadDefault(filename, target) cx_properties_load(cx_properties_config_default, cx_strcast(filename), target)
+
 
 #ifdef __cplusplus
 } // extern "C"
--- a/src/properties.c	Sun Dec 07 15:33:16 2025 +0100
+++ b/src/properties.c	Sun Dec 07 15:34:46 2025 +0100
@@ -29,6 +29,8 @@
 #include "cx/properties.h"
 
 #include <assert.h>
+#include <stdio.h>
+#include <string.h>
 
 const CxPropertiesConfig cx_properties_config_default = {
         '=',
@@ -241,180 +243,89 @@
     return CX_PROPERTIES_NO_DATA;
 }
 
-static int cx_properties_sink_map(
-        cx_attr_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 = cxMapPut(map, key, v.ptr);
-    if (r != 0) cx_strfree_a(alloc, &v);
-    return r;
-}
+#ifndef CX_PROPERTIES_LOAD_FILL_SIZE
+#define CX_PROPERTIES_LOAD_FILL_SIZE 1024
+#endif
+const unsigned cx_properties_load_fill_size = CX_PROPERTIES_LOAD_FILL_SIZE;
+#ifndef CX_PROPERTIES_LOAD_BUF_SIZE
+#define CX_PROPERTIES_LOAD_BUF_SIZE 256
+#endif
+const unsigned cx_properties_load_buf_size = CX_PROPERTIES_LOAD_BUF_SIZE;
 
-CxPropertiesSink cxPropertiesMapSink(CxMap *map) {
-    CxPropertiesSink sink;
-    sink.sink = map;
-    sink.data = (void*) cxDefaultAllocator;
-    sink.sink_func = cx_properties_sink_map;
-    return sink;
-}
+CxPropertiesStatus cx_properties_load(CxPropertiesConfig config,
+        cxstring filename, CxMap *target) {
+    // sanity check for the map
+    const bool use_cstring = cxCollectionStoresPointers(target);
+    if (!use_cstring && cxCollectionElementSize(target) != sizeof(cxmutstr)) {
+        return CX_PROPERTIES_MAP_ERROR;
+    }
 
-static int cx_properties_read_string(
-        CxProperties *prop,
-        CxPropertiesSource *src,
-        cxstring *target
-) {
-    if (prop->input.space == 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;
+    // create a duplicate to guarantee zero-termination
+    cxmutstr fname = cx_strdup(filename);
+    if (fname.ptr == NULL) {
+        return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
     }
-    return 0;
-}
 
-static int cx_properties_read_file(
-        cx_attr_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((FILE*)src->src);
-}
+    // open the file
+    FILE *f = fopen(fname.ptr, "r");
+    if (f == NULL) {
+        cx_strfree(&fname);
+        return CX_PROPERTIES_FILE_ERROR;
+    }
 
-static int cx_properties_read_init_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src
-) {
-    src->data_ptr = cxMallocDefault(src->data_size);
-    if (src->data_ptr == NULL) return 1;
-    return 0;
-}
+    // initialize the parser
+    char linebuf[cx_properties_load_buf_size];
+    char fillbuf[cx_properties_load_fill_size];
+    CxPropertiesStatus status;
+    CxProperties parser;
+    cxPropertiesInit(&parser, config);
+    cxPropertiesUseStack(&parser, linebuf, cx_properties_load_buf_size);
 
-static void cx_properties_read_clean_file(
-        cx_attr_unused CxProperties *prop,
-        CxPropertiesSource *src
-) {
-    cxFreeDefault(src->data_ptr);
-}
-
-CxPropertiesSource cxPropertiesStringSource(cxstring str) {
-    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;
-    return src;
-}
-
-CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len) {
-    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;
-    return src;
-}
-
-CxPropertiesSource cxPropertiesCstrSource(const char *str) {
-    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;
-    return src;
-}
-
-CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size) {
-    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;
-    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;  // LCOV_EXCL_LINE
+    // read/fill/parse loop
+    status = CX_PROPERTIES_NO_DATA;
+    while (true) {
+        size_t r = fread(fillbuf, 1, cx_properties_load_fill_size, f);
+        if (ferror(f)) {
+            status = CX_PROPERTIES_FILE_ERROR;
+            break;
+        }
+        if (r == 0) {
+            break;
+        }
+        if (cxPropertiesFilln(&parser, fillbuf, r)) {
+            status = CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+            break;
+        }
+        cxstring key, value;
+        while (true) {
+            status = cxPropertiesNext(&parser, &key, &value);
+            if (status != CX_PROPERTIES_NO_ERROR) {
+                break;
+            } else {
+                cxmutstr v = cx_strdup(value);
+                if (v.ptr == NULL) {
+                    status = CX_PROPERTIES_MAP_ERROR;
+                    break;
+                }
+                void *mv = use_cstring ? (void*)v.ptr : &v;
+                if (cxMapPut(target, key, mv)) {
+                    cx_strfree(&v);
+                    status = CX_PROPERTIES_MAP_ERROR;
+                    break;
+                }
+            }
+        }
+        if (status > CX_PROPERTIES_OK) {
+            break;
+        } else if (status == CX_PROPERTIES_NO_DATA) {
+            // we want to report this case differently in this function
+            status = CX_PROPERTIES_NO_ERROR;
         }
     }
 
-    // transfer the data from the source to the sink
-    CxPropertiesStatus status;
-    CxPropertiesStatus kv_status = CX_PROPERTIES_NO_DATA;
-    bool found = false;
-    while (true) {
-        // read input
-        cxstring input;
-        if (source.read_func(prop, &source, &input)) { // LCOV_EXCL_START
-            status = CX_PROPERTIES_READ_FAILED;
-            break;
-        } // LCOV_EXCL_STOP
-
-        // no more data - break
-        if (input.length == 0) {
-            if (found) {
-                // something was found, check the last kv_status
-                if (kv_status == CX_PROPERTIES_INCOMPLETE_DATA) {
-                    status = CX_PROPERTIES_INCOMPLETE_DATA;
-                } else {
-                    status = CX_PROPERTIES_NO_ERROR;
-                }
-            } else {
-                // nothing found
-                status = CX_PROPERTIES_NO_DATA;
-            }
-            break;
-        }
-
-        // set the input buffer and read the k/v-pairs
-        cxPropertiesFill(prop, input);
-
-        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;  // LCOV_EXCL_LINE
-                }
-            }
-        } 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);
-    }
-
+    // cleanup and exit
+    fclose(f);
+    cxPropertiesDestroy(&parser);
+    cx_strfree(&fname);
     return status;
 }
--- a/tests/Makefile	Sun Dec 07 15:33:16 2025 +0100
+++ b/tests/Makefile	Sun Dec 07 15:34:46 2025 +0100
@@ -94,7 +94,7 @@
  ../src/cx/json.h ../src/cx/allocator.h ../src/cx/string.h \
  ../src/cx/buffer.h ../src/cx/array_list.h ../src/cx/list.h \
  ../src/cx/collection.h ../src/cx/iterator.h ../src/cx/compare.h \
- ../src/cx/compare.h
+ ../src/cx/map.h ../src/cx/hash_key.h ../src/cx/compare.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -I../src -c $<
 
--- a/tests/test_properties.c	Sun Dec 07 15:33:16 2025 +0100
+++ b/tests/test_properties.c	Sun Dec 07 15:34:46 2025 +0100
@@ -383,75 +383,11 @@
     free(long_value);
 }
 
-CX_TEST(test_properties_load_string_to_map) {
-    CxTestingAllocator talloc;
-    cx_testing_allocator_init(&talloc);
-    CxAllocator *alloc = &talloc.base;
+CX_TEST(test_properties_load) {
+    char fname[16] = "ucxtestXXXXXX";
+    int tmpfd = mkstemp(fname);
+    FILE *f = tmpfd < 0 ? NULL : fdopen(tmpfd, "w");
     CX_TEST_DO {
-        char buffer[512];
-        CxProperties prop;
-        cxPropertiesInitDefault(&prop);
-        cxPropertiesUseStack(&prop, buffer, 512);
-
-        const char *str = "key1 = value1\nkey2 = value2\n\n#comment\n\nkey3 = value3\n";
-
-        CxMap *map = cxHashMapCreateSimple(CX_STORE_POINTERS);
-        cxDefineAdvancedDestructor(map, cxFree, alloc);
-        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"));
-
-        cxMapFree(map);
-        cxPropertiesDestroy(&prop);
-
-        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
-    }
-    cx_testing_allocator_destroy(&talloc);
-}
-
-CX_TEST(test_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");
@@ -470,21 +406,14 @@
         fprintf(f, "                         \n");
 
         fprintf(f, "\n\n\n\nlast_key = property value\n");
-
-        fflush(f);
-        fseek(f, 0, SEEK_SET);
-
+        fclose(f);
+        f = NULL;
         // preparation of test file complete
 
+        // we want to load the properties into a map of char* pointers
         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);
+        cxDefineDestructor(map, cxFreeDefault);
+        CxPropertiesStatus status = cxPropertiesLoadDefault(fname, map);
 
         CX_TEST_ASSERT(status == CX_PROPERTIES_NO_ERROR);
         CX_TEST_ASSERT(cxMapSize(map) == 5);
@@ -512,69 +441,9 @@
         free(long_key);
         free(long_value);
         cxMapFree(map);
-        cxPropertiesDestroy(&prop);
-
-        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
     }
-    cx_testing_allocator_destroy(&talloc);
-}
-
-CX_TEST(test_properties_load_incomplete) {
-    CxTestingAllocator talloc;
-    cx_testing_allocator_init(&talloc);
-    CxAllocator *alloc = &talloc.base;
-    CX_TEST_DO {
-        char buffer[512];
-        CxProperties prop;
-        cxPropertiesInitDefault(&prop);
-        cxPropertiesUseStack(&prop, buffer, 512);
-
-        CxMap *map = cxHashMapCreateSimple(CX_STORE_POINTERS);
-        cxDefineAdvancedDestructor(map, cxFree, alloc);
-        CxPropertiesSink sink = cxPropertiesMapSink(map);
-        sink.data = alloc; // use the testing allocator
-        CxPropertiesSource src = cxPropertiesCstrSource("key1 = value1\nkey2 = value2\n\n#comment\n\nkey3");
-        CxPropertiesStatus status = cxPropertiesLoad(&prop, sink, src);
-
-        CX_TEST_ASSERT(status == CX_PROPERTIES_INCOMPLETE_DATA);
-        CX_TEST_ASSERT(cxMapSize(map) == 2);
-
-        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_ASSERT(v3 == NULL);
-
-        CX_TEST_ASSERT(!strcmp(v1, "value1"));
-        CX_TEST_ASSERT(!strcmp(v2, "value2"));
-
-        // provide a source with the remaining data
-        src = cxPropertiesCstrSource(" = value3\n");
-        status = cxPropertiesLoad(&prop, sink, src);
-
-        CX_TEST_ASSERT(status == CX_PROPERTIES_NO_ERROR);
-        CX_TEST_ASSERT(cxMapSize(map) == 3);
-
-        v1 = cxMapGet(map, "key1");
-        v2 = cxMapGet(map, "key2");
-        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"));
-
-        cxMapFree(map);
-        cxPropertiesDestroy(&prop);
-
-        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
-    }
-    cx_testing_allocator_destroy(&talloc);
+    if (f) fclose(f);
+    remove(fname);
 }
 
 CX_TEST(test_properties_multiple_fill) {
@@ -684,9 +553,14 @@
     cx_test_register(suite, test_properties_next_multi);
     cx_test_register(suite, test_properties_next_part);
     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_load_incomplete);
+    cx_test_register(suite, test_properties_load);
+    // TODO: test_properties_load_empty_file
+    // TODO: test_properties_load_invalid_key
+    // TODO: test_properties_load_missing_delimiter
+    // TODO: test_properties_load_unexpected_end
+    // TODO: test_properties_load_file_not_exists
+    // TODO: test_properties_load_exceed_stack
+    // TODO: test_properties_load_incompatible_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);

mercurial