「‍」 Lingenic

hsv

(⤓.c ◇.c); γ ≜ [2026-01-27T073242.851, 2026-01-27T073242.851] ∧ |γ| = 1

/*
 * Copyright 2026 Danslav Slavenskoj, Lingenic LLC
 * License: CC0 1.0 - Public Domain
 * https://creativecommons.org/publicdomain/zero/1.0/
 * You may use this code for any purpose without attribution.
 *
 * Spec: https://hsvfile.com
 * Repo: https://github.com/LingenicLLC/HSV
 *
 * HSV - Hierarchical Separated Values
 * A text-based file format and streaming protocol using ASCII control characters.
 */

#include "hsv.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

/* Internal helper functions */

static char *strdup_range(const char *start, const char *end) {
    size_t len = end - start;
    char *s = malloc(len + 1);
    if (s) {
        memcpy(s, start, len);
        s[len] = '\0';
    }
    return s;
}

static hsv_value_t *create_string_value(const char *s) {
    hsv_value_t *v = malloc(sizeof(hsv_value_t));
    if (v) {
        v->type = HSV_TYPE_STRING;
        v->data.string = strdup(s);
    }
    return v;
}

static hsv_value_t *create_object_value(void) {
    hsv_value_t *v = malloc(sizeof(hsv_value_t));
    if (v) {
        v->type = HSV_TYPE_OBJECT;
        v->data.object.pairs = NULL;
        v->data.object.count = 0;
    }
    return v;
}

static hsv_value_t *create_array_value(void) {
    hsv_value_t *v = malloc(sizeof(hsv_value_t));
    if (v) {
        v->type = HSV_TYPE_ARRAY;
        v->data.array.items = NULL;
        v->data.array.count = 0;
    }
    return v;
}

static void object_add_pair(hsv_value_t *obj, const char *key, hsv_value_t *value) {
    if (obj->type != HSV_TYPE_OBJECT) return;

    size_t new_count = obj->data.object.count + 1;
    hsv_pair_t *new_pairs = realloc(obj->data.object.pairs, new_count * sizeof(hsv_pair_t));
    if (new_pairs) {
        obj->data.object.pairs = new_pairs;
        obj->data.object.pairs[obj->data.object.count].key = strdup(key);
        obj->data.object.pairs[obj->data.object.count].value = value;
        obj->data.object.count = new_count;
    }
}

static void array_add_item(hsv_value_t *arr, hsv_value_t *item) {
    if (arr->type != HSV_TYPE_ARRAY) return;

    size_t new_count = arr->data.array.count + 1;
    hsv_value_t **new_items = realloc(arr->data.array.items, new_count * sizeof(hsv_value_t *));
    if (new_items) {
        arr->data.array.items = new_items;
        arr->data.array.items[arr->data.array.count] = item;
        arr->data.array.count = new_count;
    }
}

/* Split string by separator, respecting SO/SI nesting */
typedef struct {
    char **parts;
    size_t count;
} split_result_t;

static split_result_t split_respecting_nesting(const char *text, char sep) {
    split_result_t result = {NULL, 0};

    const char *start = text;
    const char *p = text;
    int depth = 0;

    while (*p) {
        if (*p == HSV_SO) {
            depth++;
        } else if (*p == HSV_SI) {
            depth--;
        } else if (*p == sep && depth == 0) {
            /* Add part */
            result.parts = realloc(result.parts, (result.count + 1) * sizeof(char *));
            result.parts[result.count] = strdup_range(start, p);
            result.count++;
            start = p + 1;
        }
        p++;
    }

    /* Add final part */
    result.parts = realloc(result.parts, (result.count + 1) * sizeof(char *));
    result.parts[result.count] = strdup(start);
    result.count++;

    return result;
}

static void free_split_result(split_result_t *r) {
    for (size_t i = 0; i < r->count; i++) {
        free(r->parts[i]);
    }
    free(r->parts);
    r->parts = NULL;
    r->count = 0;
}

static hsv_value_t *parse_value(const char *text);
static hsv_value_t *parse_object(const char *text);

static hsv_value_t *parse_value(const char *text) {
    size_t len = strlen(text);

    /* Check for nested structure (SO at start, SI at end) */
    if (len >= 2 && text[0] == HSV_SO && text[len - 1] == HSV_SI) {
        char *inner = strdup_range(text + 1, text + len - 1);
        hsv_value_t *obj = parse_object(inner);
        free(inner);
        return obj;
    }

    /* Check for array */
    if (strchr(text, HSV_GS)) {
        hsv_value_t *arr = create_array_value();
        split_result_t parts = split_respecting_nesting(text, HSV_GS);
        for (size_t i = 0; i < parts.count; i++) {
            array_add_item(arr, parse_value(parts.parts[i]));
        }
        free_split_result(&parts);
        return arr;
    }

    return create_string_value(text);
}

static hsv_value_t *parse_object(const char *text) {
    hsv_value_t *obj = create_object_value();

    split_result_t props = split_respecting_nesting(text, HSV_RS);
    for (size_t i = 0; i < props.count; i++) {
        split_result_t kv = split_respecting_nesting(props.parts[i], HSV_US);
        if (kv.count >= 2) {
            /* Join remaining parts with US for value */
            size_t value_len = 0;
            for (size_t j = 1; j < kv.count; j++) {
                value_len += strlen(kv.parts[j]) + 1;
            }
            char *value = malloc(value_len);
            value[0] = '\0';
            for (size_t j = 1; j < kv.count; j++) {
                if (j > 1) {
                    size_t len = strlen(value);
                    value[len] = HSV_US;
                    value[len + 1] = '\0';
                }
                strcat(value, kv.parts[j]);
            }

            object_add_pair(obj, kv.parts[0], parse_value(value));
            free(value);
        }
        free_split_result(&kv);
    }
    free_split_result(&props);

    return obj;
}

hsv_document_t *hsv_parse(const char *text) {
    hsv_document_t *doc = malloc(sizeof(hsv_document_t));
    if (!doc) return NULL;

    doc->header = NULL;
    doc->records = NULL;
    doc->record_count = 0;

    const char *p = text;

    while (*p) {
        if (*p == HSV_SOH) {
            /* Find STX */
            const char *stx = strchr(p + 1, HSV_STX);
            if (!stx) {
                p++;
                continue;
            }

            /* Parse header */
            char *header_content = strdup_range(p + 1, stx);
            doc->header = parse_object(header_content);
            free(header_content);

            /* Find ETX */
            const char *etx = strchr(stx + 1, HSV_ETX);
            if (!etx) {
                p = stx + 1;
                continue;
            }

            /* Parse records */
            char *data_content = strdup_range(stx + 1, etx);
            split_result_t records = split_respecting_nesting(data_content, HSV_FS);
            for (size_t i = 0; i < records.count; i++) {
                hsv_value_t *obj = parse_object(records.parts[i]);
                if (obj->data.object.count > 0) {
                    doc->records = realloc(doc->records, (doc->record_count + 1) * sizeof(hsv_value_t *));
                    doc->records[doc->record_count] = obj;
                    doc->record_count++;
                } else {
                    hsv_free_value(obj);
                }
            }
            free_split_result(&records);
            free(data_content);

            p = etx + 1;
        } else if (*p == HSV_STX) {
            /* Find ETX */
            const char *etx = strchr(p + 1, HSV_ETX);
            if (!etx) {
                p++;
                continue;
            }

            /* Parse records */
            char *data_content = strdup_range(p + 1, etx);
            split_result_t records = split_respecting_nesting(data_content, HSV_FS);
            for (size_t i = 0; i < records.count; i++) {
                hsv_value_t *obj = parse_object(records.parts[i]);
                if (obj->data.object.count > 0) {
                    doc->records = realloc(doc->records, (doc->record_count + 1) * sizeof(hsv_value_t *));
                    doc->records[doc->record_count] = obj;
                    doc->record_count++;
                } else {
                    hsv_free_value(obj);
                }
            }
            free_split_result(&records);
            free(data_content);

            p = etx + 1;
        } else {
            p++;
        }
    }

    return doc;
}

void hsv_free_value(hsv_value_t *value) {
    if (!value) return;

    switch (value->type) {
        case HSV_TYPE_STRING:
            free(value->data.string);
            break;
        case HSV_TYPE_ARRAY:
            for (size_t i = 0; i < value->data.array.count; i++) {
                hsv_free_value(value->data.array.items[i]);
            }
            free(value->data.array.items);
            break;
        case HSV_TYPE_OBJECT:
            for (size_t i = 0; i < value->data.object.count; i++) {
                free(value->data.object.pairs[i].key);
                hsv_free_value(value->data.object.pairs[i].value);
            }
            free(value->data.object.pairs);
            break;
    }

    free(value);
}

void hsv_free_document(hsv_document_t *doc) {
    if (!doc) return;

    hsv_free_value(doc->header);
    for (size_t i = 0; i < doc->record_count; i++) {
        hsv_free_value(doc->records[i]);
    }
    free(doc->records);
    free(doc);
}

const char *hsv_get_string(hsv_value_t *obj, const char *key) {
    hsv_value_t *v = hsv_get(obj, key);
    if (v && v->type == HSV_TYPE_STRING) {
        return v->data.string;
    }
    return NULL;
}

hsv_value_t *hsv_get(hsv_value_t *obj, const char *key) {
    if (!obj || obj->type != HSV_TYPE_OBJECT) return NULL;

    for (size_t i = 0; i < obj->data.object.count; i++) {
        if (strcmp(obj->data.object.pairs[i].key, key) == 0) {
            return obj->data.object.pairs[i].value;
        }
    }
    return NULL;
}