Fri, 23 May 2025 12:44:24 +0200
make test-compile depend on both static and shared
the shared lib is not needed for the tests,
but when run with coverage, gcov will be confused
when outdated line information is available from
a previous shared build
/* * 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/properties.h" #include <assert.h> 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) { cxBufferDestroy(&prop->input); cxBufferDestroy(&prop->buffer); } int cxPropertiesFilln( CxProperties *prop, const char *buf, size_t len ) { if (cxBufferEof(&prop->input)) { // destroy a possible previously initialized buffer cxBufferDestroy(&prop->input); cxBufferInit(&prop->input, (void*) buf, len, NULL, CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_AUTO_EXTEND); prop->input.size = len; } else { if (cxBufferAppend(buf, 1, len, &prop->input) < len) return -1; } return 0; } void cxPropertiesUseStack( CxProperties *prop, char *buf, size_t capacity ) { cxBufferInit(&prop->buffer, buf, capacity, NULL, CX_BUFFER_COPY_ON_EXTEND); } CxPropertiesStatus cxPropertiesNext( CxProperties *prop, cxstring *key, cxstring *value ) { // check if we have a text buffer if (prop->input.space == NULL) { return CX_PROPERTIES_NULL_INPUT; } // a pointer to the buffer we want to read from CxBuffer *current_buffer = &prop->input; // check if we have rescued data if (!cxBufferEof(&prop->buffer)) { // check if we can now get a complete line cxstring input = cx_strn(prop->input.space + prop->input.pos, prop->input.size - prop->input.pos); cxstring nl = cx_strchr(input, '\n'); if (nl.length > 0) { // we add as much data to the rescue buffer as we need // to complete the line size_t len_until_nl = (size_t)(nl.ptr - input.ptr) + 1; if (cxBufferAppend(input.ptr, 1, len_until_nl, &prop->buffer) < len_until_nl) { return CX_PROPERTIES_BUFFER_ALLOC_FAILED; } // advance the position in the input buffer prop->input.pos += len_until_nl; // we now want to read from the rescue buffer current_buffer = &prop->buffer; } else { // still not enough data, copy input buffer to internal buffer if (cxBufferAppend(input.ptr, 1, input.length, &prop->buffer) < input.length) { return CX_PROPERTIES_BUFFER_ALLOC_FAILED; } // reset the input buffer (make way for a re-fill) cxBufferReset(&prop->input); return CX_PROPERTIES_INCOMPLETE_DATA; } } 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 (!cxBufferEof(current_buffer)) { const char *buf = current_buffer->space + current_buffer->pos; size_t len = current_buffer->size - current_buffer->pos; /* * First we check if we have at least one line. We also get indices of * 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, use the rescue buffer assert(current_buffer != &prop->buffer); // make sure that the rescue buffer does not already contain something assert(cxBufferEof(&prop->buffer)); if (prop->buffer.space == NULL) { // initialize a rescue buffer, if the user did not provide one cxBufferInit(&prop->buffer, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND); } else { // from a previous rescue there might be already read data // reset the buffer to avoid unnecessary buffer extension cxBufferReset(&prop->buffer); } if (cxBufferAppend(buf, 1, len, &prop->buffer) < len) { return CX_PROPERTIES_BUFFER_ALLOC_FAILED; } // reset the input buffer (make way for a re-fill) cxBufferReset(&prop->input); return CX_PROPERTIES_INCOMPLETE_DATA; } 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 { // skip blank line // if it was the rescue buffer, return to the original buffer if (current_buffer == &prop->buffer) { // assert that the rescue buffer really does not contain more data assert(current_buffer->pos + i + 1 == current_buffer->size); // reset the rescue buffer, but don't destroy it! cxBufferReset(&prop->buffer); // continue with the input buffer current_buffer = &prop->input; } else { // if it was the input buffer already, just advance the position current_buffer->pos += i + 1; } continue; } } else { cxstring k = cx_strn(buf, delimiter_index); 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; current_buffer->pos += i + 1; assert(current_buffer->pos <= current_buffer->size); return CX_PROPERTIES_NO_ERROR; } else { return CX_PROPERTIES_INVALID_EMPTY_KEY; } } } // when we come to this point, all data must have been read assert(cxBufferEof(&prop->buffer)); assert(cxBufferEof(&prop->input)); 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 = cx_map_put_cxstr(map, key, v.ptr); if (r != 0) cx_strfree_a(alloc, &v); return r; } CxPropertiesSink cxPropertiesMapSink(CxMap *map) { CxPropertiesSink sink; sink.sink = map; sink.data = (void*) cxDefaultAllocator; sink.sink_func = cx_properties_sink_map; return sink; } 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; } 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); } 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; } 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; } } // 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)) { status = CX_PROPERTIES_READ_FAILED; break; } // 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; } } } 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); } return status; }