Sun, 07 Dec 2025 15:34:46 +0100
properties.h: removes the source/sink API and adds a new cxPropertiesLoad()
resolves #610
--- 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);