diff options
| author | Muhammad Moinur Rahman <bofh@FreeBSD.org> | 2026-07-17 20:29:58 +0000 |
|---|---|---|
| committer | Muhammad Moinur Rahman <bofh@FreeBSD.org> | 2026-07-17 20:29:58 +0000 |
| commit | d7ea4aaa3e204e5c22dadb94078f3638f6b3b505 (patch) | |
| tree | ad4c2d5608738036e4f3ee8aafcde836143b6af2 | |
| parent | 1dd83cf7e527ff29d734f6a2c8f9b61d39b41d94 (diff) | |
| -rw-r--r-- | CLAUDE.md | 82 | ||||
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | ChangeLog.md | 10 | ||||
| -rw-r--r-- | configure.ac | 4 | ||||
| -rw-r--r-- | include/ucl.h | 23 | ||||
| -rw-r--r-- | src/ucl_emitter.c | 12 | ||||
| -rw-r--r-- | src/ucl_hash.c | 11 | ||||
| -rw-r--r-- | src/ucl_hash.h | 6 | ||||
| -rw-r--r-- | src/ucl_util.c | 31 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 12 | ||||
| -rw-r--r-- | tests/basic/issue312.in | 4 | ||||
| -rw-r--r-- | tests/basic/issue312.res | 13 |
12 files changed, 196 insertions, 14 deletions
diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000000..772ee60d423e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +libucl is a C library for parsing UCL (Universal Configuration Language) — an nginx-inspired config format that is a superset of JSON. It also reads/writes MessagePack, emits YAML, and reads S-expressions. Version 0.9.4. + +## Build Commands + +```bash +# Standard CMake build +cmake -B build && cmake --build build + +# Build with sanitizers +cmake -B build -DENABLE_SANITIZERS=ON -DSANITIZE_TYPE=address && cmake --build build + +# Build with utilities (ucl-tool, objdump, chargen) +cmake -B build -DENABLE_UTILS=ON && cmake --build build + +# Build with Lua bindings +cmake -B build -DENABLE_LUA=ON && cmake --build build +``` + +## Running Tests + +```bash +# Run all tests via CTest +cd build && ctest + +# Run a specific test (test names: test_basic, test_schema, test_msgpack, test_generate, test_speed) +cd build && ctest -R test_basic + +# Run test binary directly (needs env vars) +TEST_DIR=tests TEST_OUT_DIR=build/tests TEST_BINARY_DIR=build/tests ./tests/basic.test +``` + +Tests use shell wrapper scripts (`tests/*.test`) that invoke compiled test binaries. Basic tests parse `.in` files and compare output against `.res` files in `tests/basic/`. Schema tests run JSON Schema v4 test suites from `tests/schema/*.json`. + +## Architecture + +**Core source files** (all in `src/`): + +- `ucl_parser.c` — State-machine parser handling UCL, JSON, and msgpack input. The main parsing loop, macro processing (`.include`, `.priority`), and variable expansion live here. This is the largest and most complex file. +- `ucl_emitter.c` — Tree-walking emitter dispatching to format-specific handlers (JSON, YAML, config, msgpack). Uses a context/ops pattern where `ucl_emitter_context` holds state and `ucl_emitter_operations` is a vtable of format callbacks. +- `ucl_emitter_streamline.c` — Streaming emitter API that builds output incrementally without requiring a complete object tree. +- `ucl_emitter_utils.c` — Shared emitter helpers: number formatting, string escaping, indentation. +- `ucl_schema.c` — JSON Schema draft v4 validator. Recursive descent over schema keywords (type, properties, allOf/anyOf/oneOf, etc.). +- `ucl_hash.c` — Hash table wrapping uthash with optional case-insensitive keys. Each `ucl_object_t` of type `UCL_OBJECT` stores children in one of these. +- `ucl_util.c` — Object manipulation API (creation, lookup, iteration, type conversion), file I/O, and the `.include` macro implementation. +- `ucl_msgpack.c` — MessagePack serialization/deserialization. +- `ucl_sexp.c` — S-expression (canonical form) parser. + +**Key headers**: + +- `include/ucl.h` — Public C API. All external types and functions. +- `include/ucl++.h` — C++ wrapper with `Ucl` class. +- `src/ucl_internal.h` — Internal parser state, chunk management, macro structures. + +**Bundled dependencies** (header-only, in-tree): + +- `uthash/` — Hash table macros (uthash, utlist, utstring) +- `klib/` — kvec (dynamic arrays) + +**Object model**: The central type is `ucl_object_t` (defined in `ucl.h`). Objects form a tree; each node has a type (int, float, string, boolean, object, array, null, userdata), a key, and a next pointer for implicit arrays (duplicate keys). Objects use reference counting (`ucl_object_ref`/`ucl_object_unref`). + +**Parser lifecycle**: Create with `ucl_parser_new()`, feed data with `ucl_parser_add_string()`/`ucl_parser_add_file()`/`ucl_parser_add_fd()`, retrieve result with `ucl_parser_get_object()`, free with `ucl_parser_free()`. The parser processes input in chunks and maintains a state stack. + +## Test Data Convention + +Basic parse tests: `tests/basic/N.in` (input) + `tests/basic/N.res` (expected output). To add a parse test, create a new `.in`/`.res` pair. The test harness also exercises FD-based parsing, JSON/YAML output modes, and comment/macro preservation modes on every input file. + +## Notable CMake Options + +| Option | Default | Purpose | +|--------|---------|---------| +| `ENABLE_SANITIZERS` | OFF | AddressSanitizer/UBSan/etc. | +| `SANITIZE_TYPE` | address | Which sanitizer (address, thread, undefined, memory, leak) | +| `ENABLE_URL_INCLUDE` | OFF | `.include` from URLs (needs libcurl or libfetch) | +| `ENABLE_URL_SIGN` | OFF | Signature verification for includes (needs OpenSSL) | +| `ENABLE_LUA` | OFF | Lua bindings (`lua/lua_ucl.c`) | +| `BUILD_SHARED_LIBS` | OFF | Build shared instead of static library | diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bb2622a8622..23b2ee054f52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ PROJECT(libucl C) SET(LIBUCL_VERSION_MAJOR 0) SET(LIBUCL_VERSION_MINOR 9) -SET(LIBUCL_VERSION_PATCH 3) +SET(LIBUCL_VERSION_PATCH 4) SET(LIBUCL_VERSION "${LIBUCL_VERSION_MAJOR}.${LIBUCL_VERSION_MINOR}.${LIBUCL_VERSION_PATCH}") diff --git a/ChangeLog.md b/ChangeLog.md index bf76b8e517e9..777014e71133 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,15 @@ # Version history +## Libucl 0.9.4 + +- Fix double comma in JSON emission for consecutive implicit arrays (Fixes #312) +- Add `ucl_object_iterate_end()` to fix iterator memory leak on early exit +- Fix potential null pointer dereference in `ucl_hash_sort` function +- Fix `ucl_object_replace_key` to return true on new insert +- Reset length during object copy +- Add null check in `ucl_object_reserve` +- Skip building POSIX-only tests on Windows + ## Libucl 0.9.3 - Fix invalid JSON emission when merging objects (Fixes #312) diff --git a/configure.ac b/configure.ac index 93d200505b5d..2046de221a6b 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ m4_define([maj_ver], [0]) m4_define([med_ver], [9]) -m4_define([min_ver], [3]) -m4_define([so_version], [9:0:3]) +m4_define([min_ver], [4]) +m4_define([so_version], [9:0:4]) m4_define([ucl_version], [maj_ver.med_ver.min_ver]) AC_PREREQ([2.70]) diff --git a/include/ucl.h b/include/ucl.h index 96ddaa9cb987..bb75ae94810f 100644 --- a/include/ucl.h +++ b/include/ucl.h @@ -862,6 +862,29 @@ UCL_EXTERN const ucl_object_t *ucl_object_iterate_with_error(const ucl_object_t #define ucl_object_iterate(ob, it, ev) ucl_object_iterate_with_error((ob), (it), (ev), NULL) /** + * Free resources associated with an inline iterator when iteration is + * abandoned before completion. Only needed for UCL_OBJECT iteration where + * internal heap state is allocated. Safe to call with a NULL iterator or + * on non-object types. + * + * Example usage: + * ucl_object_iter_t it = NULL; + * while ((cur = ucl_iterate_object(obj, &it, true))) { + * if (error) { + * ucl_object_iterate_end(obj, &it); + * return; + * } + * } + * + * @param obj the object being iterated + * @param iter pointer to the iterator to free + */ +UCL_EXTERN void ucl_object_iterate_end(const ucl_object_t *obj, + ucl_object_iter_t *iter); + +#define ucl_iterate_object_end ucl_object_iterate_end + +/** * Create new safe iterator for the specified object * @param obj object to iterate * @return new iterator object that should be used with safe iterators API only diff --git a/src/ucl_emitter.c b/src/ucl_emitter.c index 84c1b13e1865..bcea5b037e70 100644 --- a/src/ucl_emitter.c +++ b/src/ucl_emitter.c @@ -353,10 +353,16 @@ ucl_emitter_common_start_object(struct ucl_emitter_context *ctx, else { /* Expand implicit arrays */ if (cur->next != NULL) { - if (first_key) { - ucl_add_tabs(func, ctx->indent, compact); + if (!first_key) { + if (compact) { + func->ucl_emitter_append_character(',', 1, func->ud); + } + else { + func->ucl_emitter_append_len(",\n", 2, func->ud); + } } - ucl_emitter_common_start_array(ctx, cur, first_key, true, compact); + ucl_add_tabs(func, ctx->indent, compact); + ucl_emitter_common_start_array(ctx, cur, true, true, compact); ucl_emitter_common_end_array(ctx, cur, compact); } else { diff --git a/src/ucl_hash.c b/src/ucl_hash.c index d648f405951b..3fd9b29c23ca 100644 --- a/src/ucl_hash.c +++ b/src/ucl_hash.c @@ -443,6 +443,14 @@ bool ucl_hash_iter_has_next(ucl_hash_t *hashlin, ucl_hash_iter_t iter) return it->cur != NULL; } +void ucl_hash_iterate_free(ucl_hash_iter_t iter) +{ + struct ucl_hash_real_iter *it = (struct ucl_hash_real_iter *) (iter); + + if (it != NULL) { + UCL_FREE(sizeof(*it), it); + } +} const ucl_object_t * ucl_hash_search(ucl_hash_t *hashlin, const char *key, unsigned keylen) @@ -610,6 +618,9 @@ ucl_hash_cmp_case_sens(const void *a, const void *b) void ucl_hash_sort(ucl_hash_t *hashlin, enum ucl_object_keys_sort_flags fl) { + if (hashlin == NULL) { + return; + } if (fl & UCL_SORT_KEYS_ICASE) { DL_SORT(hashlin->head, ucl_hash_cmp_icase); diff --git a/src/ucl_hash.h b/src/ucl_hash.h index 95fe206b677e..ca77e208e2ac 100644 --- a/src/ucl_hash.h +++ b/src/ucl_hash.h @@ -98,6 +98,12 @@ const void *ucl_hash_iterate2(ucl_hash_t *hashlin, ucl_hash_iter_t *iter, int *e bool ucl_hash_iter_has_next(ucl_hash_t *hashlin, ucl_hash_iter_t iter); /** + * Free a hash iterator (used when iteration is abandoned early) + * @param iter iterator to free, may be NULL + */ +void ucl_hash_iterate_free(ucl_hash_iter_t iter); + +/** * Reserves space in hash * @return true on sucess, false on failure (e.g. ENOMEM) * @param hashlin diff --git a/src/ucl_util.c b/src/ucl_util.c index 3566632275a5..a56d8b27ad65 100644 --- a/src/ucl_util.c +++ b/src/ucl_util.c @@ -2435,9 +2435,7 @@ ucl_object_insert_key_common(ucl_object_t *top, ucl_object_t *elt, if (found == NULL) { top->value.ov = ucl_hash_insert_object(top->value.ov, elt, false); top->len++; - if (replace) { - ret = false; - } + /* Key was inserted - return true regardless of replace flag */ } else { if (replace) { @@ -2767,6 +2765,20 @@ ucl_object_iterate_with_error(const ucl_object_t *obj, ucl_object_iter_t *iter, return NULL; } +void +ucl_object_iterate_end(const ucl_object_t *obj, ucl_object_iter_t *iter) +{ + if (iter == NULL || *iter == NULL) { + return; + } + + if (obj != NULL && obj->type == UCL_OBJECT) { + ucl_hash_iterate_free(*iter); + } + + *iter = NULL; +} + enum ucl_safe_iter_flags { UCL_ITERATE_FLAG_UNDEFINED = 0, UCL_ITERATE_FLAG_INSIDE_ARRAY, @@ -3028,6 +3040,16 @@ bool ucl_object_reserve(ucl_object_t *obj, size_t reserved) if (obj->type == UCL_ARRAY) { UCL_ARRAY_GET(vec, obj); + if (vec == NULL) { + /* Allocate array storage if not present (e.g., copied empty array) */ + vec = UCL_ALLOC(sizeof(*vec)); + if (vec == NULL) { + return false; + } + kv_init(*vec); + obj->value.av = (void *)vec; + } + if (vec->m < reserved) { /* Preallocate some space for arrays */ kv_resize_safe(ucl_object_t *, *vec, reserved, e0); @@ -3657,8 +3679,9 @@ ucl_object_copy_internal(const ucl_object_t *other, bool allow_array) } if (other->type == UCL_ARRAY || other->type == UCL_OBJECT) { - /* reset old value */ + /* reset old value and length since we will re-add elements below */ memset(&new->value, 0, sizeof(new->value)); + new->len = 0; while ((cur = ucl_object_iterate(other, &it, true)) != NULL) { if (other->type == UCL_ARRAY) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e66c3d10a60a..4d91bfa49cbb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,10 +22,14 @@ macro(add_ucl_test testname sourcefile wrapper) ENDIF() endmacro() -# Build test binaries always (not just for testing) -add_ucl_test(test_basic test_basic.c basic.test) -add_ucl_test(test_speed test_speed.c speed.test) -add_ucl_test(test_schema test_schema.c schema.test) +# Tests using POSIX APIs (mmap, STDIN_FILENO, etc.) - skip on Windows +IF(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_ucl_test(test_basic test_basic.c basic.test) + add_ucl_test(test_speed test_speed.c speed.test) + add_ucl_test(test_schema test_schema.c schema.test) +ENDIF() + +# Portable tests add_ucl_test(test_msgpack test_msgpack.c msgpack.test) add_ucl_test(test_generate test_generate.c generate.test) diff --git a/tests/basic/issue312.in b/tests/basic/issue312.in new file mode 100644 index 000000000000..ec46cbe804da --- /dev/null +++ b/tests/basic/issue312.in @@ -0,0 +1,4 @@ +section_a { key_1 = "value_1"; } +section_a { key_2 = "value_2"; } +section_b { key_3 = "value_3"; } +section_b { key_4 = "value_4"; } diff --git a/tests/basic/issue312.res b/tests/basic/issue312.res new file mode 100644 index 000000000000..739174f9f230 --- /dev/null +++ b/tests/basic/issue312.res @@ -0,0 +1,13 @@ +section_a { + key_1 = "value_1"; +} +section_a { + key_2 = "value_2"; +} +section_b { + key_3 = "value_3"; +} +section_b { + key_4 = "value_4"; +} + |
