src/properties.c

Sat, 12 Oct 2024 19:34:19 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 12 Oct 2024 19:34:19 +0200
changeset 924
3c90dfc35f06
parent 923
45da884269c8
permissions
-rw-r--r--

add implementation for the properties parser

relates to #429

/*
 * 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 <string.h>
#include <assert.h>

static const int CX_PROPERTIES_FLAG_USE_STACK = 0x01;

const CxPropertiesConfig cx_properties_config_default = {
        '=',
        //'\\',
        '#',
        '\0',
        '\0'
};

void cxPropertiesInit(
        CxProperties *prop,
        CxPropertiesConfig config
) {
    memset(prop, 0, sizeof(CxProperties));
    prop->config = config;
}

void cxPropertiesDestroy(CxProperties *prop) {
    if (0 == (prop->flags & CX_PROPERTIES_FLAG_USE_STACK)) {
        free(prop->buf);
    }
    prop->buf = NULL;
    prop->buf_capacity = prop->buf_size = 0;
}

static int cx_properties_ensure_buf_capacity(CxProperties *prop, size_t cap) {
    if (prop->buf_capacity >= cap) {
        return 0;
    }

    // not enough capacity - are we on the stack right now?
    if ((prop->flags & CX_PROPERTIES_FLAG_USE_STACK) != 0) {
        // move to the heap
        char *newbuf = malloc(cap);
        if (newbuf == NULL) return 1;
        memcpy(newbuf, prop->buf, prop->buf_size);
        prop->buf = newbuf;
        prop->flags &= CX_PROPERTIES_FLAG_USE_STACK;
    } else {
        // we are on the heap already, reallocate
        // this is legit, because realloc() behaves like malloc() when the
        // current pointer is NULL
        char *newbuf = realloc(prop->buf, cap);
        if (newbuf == NULL) return 1;
        prop->buf = newbuf;
    }

    // store new capacity and return
    prop->buf_capacity = cap;
    return 0;
}

static int cx_properties_rescuen_input(CxProperties *prop, size_t len) {
    if (cx_properties_ensure_buf_capacity(prop, prop->buf_size + len)) {
        return 1;
    }
    const char *src = prop->text + prop->text_pos;
    char *dest = prop->buf + prop->buf_size;
    memcpy(dest, src, len);
    prop->buf_size += len;
    return 0;
}

static int cx_properties_rescue_input(CxProperties *prop) {
    // someone fucked around with our integers, exit immediately
    if (prop->text_pos > prop->text_size) return 0;

    // determine the bytes needed
    size_t len = prop->text_size - prop->text_pos;

    return cx_properties_rescuen_input(prop, len);
}

void cxPropertiesInput(
        CxProperties *prop,
        const char *buf,
        size_t len
) {
    prop->text = buf;
    prop->text_size = len;
    prop->text_pos = 0;
}

int cxPropertiesFill(
        CxProperties *prop,
        const char *buf,
        size_t len
) {
    if (cx_properties_rescue_input(prop)) return 1;
    cxPropertiesInput(prop, buf, len);
    return 0;
}

void cxPropertiesUseStack(
        CxProperties *prop,
        char *buf,
        size_t capacity
) {
    assert(buf == NULL);
    prop->buf = buf;
    prop->buf_capacity = capacity;
    prop->buf_size = 0;
    prop->flags |= CX_PROPERTIES_FLAG_USE_STACK;
}

enum cx_properties_status cxPropertiesNext(
        CxProperties *prop,
        cxstring *key,
        cxstring *value
) {
    // check if we have a text buffer
    if (prop->text == NULL) {
        return CX_PROPERTIES_NULL_INPUT;
    }
    // check if we have rescued data
    if (prop->buf_size > 0) {
        // check if we can now get a complete line
        const char *buf = prop->text + prop->text_pos;
        size_t len = prop->text_size - prop->text_pos;
        cxstring str = cx_strn(buf, len);
        cxstring nl = cx_strchr(str, '\n');
        if(nl.length > 0) {
            // we add as much data to the rescue buffer as we need
            // to complete the line
            size_t len_until_nl = (size_t)(nl.ptr - buf) + 1;

            if (cx_properties_rescuen_input(prop, len_until_nl)) {
                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
            }

            // the tmp buffer contains exactly one line now
            // we use a trick here: we swap the buffers and recurse
            const char *orig_text = prop->text;
            size_t orig_size = prop->text_size;
            prop->text = prop->buf;
            prop->text_size = prop->buf_size;
            prop->text_pos = 0;
            prop->buf_size = 0;

            enum cx_properties_status result;
            result = cxPropertiesNext(prop, key, value);

            // restore original buffer
            prop->text = orig_text;
            prop->text_size = orig_size;

            // set the position to after the newline
            prop->text_pos = len_until_nl;

            // check the result
            if (result == CX_PROPERTIES_NO_ERROR) {
                // reset the rescue buffer and return with the result
                prop->buf_size = 0;
                return result;
            } else if (result == CX_PROPERTIES_NO_DATA) {
                // rescue buffer contained only blanks or comments
                // reset the rescue buffer and retry with text buffer
                prop->buf_size = 0;
                return cxPropertiesNext(prop, key, value);
            } else {
                // CX_PROPERTIES_INCOMPLETE_DATA is not possible
                // so it must have been another error
                // do not reset the rescue buffer and return the error
                return result;
            }
        } else {
            // still not enough data
            return CX_PROPERTIES_INCOMPLETE_DATA;
        }
    }

    char comment1 = prop->config.comment1;
    char comment2 = prop->config.comment2;
    char comment3 = prop->config.comment3;
    char delimiter = prop->config.delimiter;

    // get one line and parse it
    while (prop->text_pos < prop->text_size) {
        const char *buf = prop->text + prop->text_pos;
        size_t len = prop->text_size - prop->text_pos;

        /*
         * First we check if we have at least one line. We also get indices of
         * delimiter and comment chars
         */
        size_t delimiter_index = 0;
        size_t comment_index = 0;
        bool has_comment = false;

        size_t i = 0;
        char c = 0;
        for (; i < len; i++) {
            c = buf[i];
            if (c == comment1 || c == comment2 || c == comment3) {
                if (comment_index == 0) {
                    comment_index = i;
                    has_comment = true;
                }
            } else if (c == delimiter) {
                if (delimiter_index == 0 && !has_comment) {
                    delimiter_index = i;
                }
            } else if (c == '\n') {
                break;
            }
        }

        if (c != '\n') {
            // we don't have enough data for a line
            return CX_PROPERTIES_INCOMPLETE_DATA;
        }

        cxstring line = has_comment ?
                        cx_strn(buf, comment_index) :
                        cx_strn(buf, i);
        // check line
        if (delimiter_index == 0) {
            // if line is not blank ...
            line = cx_strtrim(line);
            // ... either no delimiter found, or key is empty
            if (line.length > 0) {
                if (line.ptr[0] == delimiter) {
                    return CX_PROPERTIES_INVALID_EMPTY_KEY;
                } else {
                    return CX_PROPERTIES_INVALID_MISSING_DELIMITER;
                }
            }
        } else {
            cxstring k = cx_strn(buf, delimiter_index);
            cxstring val = cx_strn(
                    buf + delimiter_index + 1,
                    line.length - delimiter_index - 1);
            k = cx_strtrim(k);
            val = cx_strtrim(val);
            if (k.length > 0) {
                *key = k;
                *value = val;
                prop->text_pos += i + 1;
                assert(prop->text_pos <= prop->text_size);
                return CX_PROPERTIES_NO_ERROR;
            } else {
                return CX_PROPERTIES_INVALID_EMPTY_KEY;
            }
        }

        prop->text_pos += i + 1;
    }

    // when we come to this point, all data must have been read
    assert(prop->text_pos == prop->text_size);
    return CX_PROPERTIES_NO_DATA;
}

mercurial