Sun, 07 Dec 2025 15:34:46 +0100
properties.h: removes the source/sink API and adds a new cxPropertiesLoad()
resolves #610
/* * 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> #include <stdio.h> #include <string.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); } void cxPropertiesReset(CxProperties *prop) { CxPropertiesConfig config = prop->config; cxPropertiesDestroy(prop); cxPropertiesInit(prop, config); } 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; // LCOV_EXCL_LINE } // 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; } #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; 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; } // 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 } // open the file FILE *f = fopen(fname.ptr, "r"); if (f == NULL) { cx_strfree(&fname); return CX_PROPERTIES_FILE_ERROR; } // 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); // 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; } } // cleanup and exit fclose(f); cxPropertiesDestroy(&parser); cx_strfree(&fname); return status; }