summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMuhammad Moinur Rahman <bofh@FreeBSD.org>2026-07-17 20:29:58 +0000
committerMuhammad Moinur Rahman <bofh@FreeBSD.org>2026-07-17 20:29:58 +0000
commitd7ea4aaa3e204e5c22dadb94078f3638f6b3b505 (patch)
treead4c2d5608738036e4f3ee8aafcde836143b6af2
parent1dd83cf7e527ff29d734f6a2c8f9b61d39b41d94 (diff)
-rw-r--r--CLAUDE.md82
-rw-r--r--CMakeLists.txt2
-rw-r--r--ChangeLog.md10
-rw-r--r--configure.ac4
-rw-r--r--include/ucl.h23
-rw-r--r--src/ucl_emitter.c12
-rw-r--r--src/ucl_hash.c11
-rw-r--r--src/ucl_hash.h6
-rw-r--r--src/ucl_util.c31
-rw-r--r--tests/CMakeLists.txt12
-rw-r--r--tests/basic/issue312.in4
-rw-r--r--tests/basic/issue312.res13
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";
+}
+