From 296f589ee596bc3411d4ab838e14a91dfe2531b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Wed, 9 Sep 2026 22:50:20 +0100 Subject: [PATCH] fix: an empty array should override an array of tables during merge is_array_of_tables() returns true for an empty array too, since a loop over zero elements never finds one that fails the TOML_TABLE check. That was fine on its own, but datum_merge()'s TOML_ARRAY case treats "both sides are arrays of tables" as a signal to append src onto dst rather than override it, and an empty array happens to satisfy that check vacuously. So merging a document that explicitly sets an existing array-of-tables key to an empty array (arr = []) doesn't clear it as intended. Instead the merge takes the append branch, appends zero elements from the empty array, and leaves dst's original array of tables completely untouched: r1: [[arr]] x = 1 r2: arr = [] toml_merge(r1, r2) -> arr still has one table, size stays 1, when the override to [] should have taken effect Excluding the empty case from is_array_of_tables() makes it fall through to the plain override path instead, same as any other non-array-of-tables src value. Added two tests to test/merge/test1.c covering both directions: an array of tables overridden by an empty array (the bug), and an empty array overridden by an array of tables (already worked, kept as a sanity check since the fix touches the same condition). --- src/tomlc17.c | 8 ++++++-- test/merge/test1.c | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/tomlc17.c b/src/tomlc17.c index 883a693..59aa326 100644 --- a/src/tomlc17.c +++ b/src/tomlc17.c @@ -665,9 +665,13 @@ static int datum_copy(toml_datum_t *dst, toml_datum_t src, srcmap_t *sm, return -1; } -// Check if datum is an array of tables. +// Check if datum is an array of tables. An empty array is deliberately +// excluded: it has no elements to type-check, so treating it as an array +// of tables here would be true only vacuously, and datum_merge's caller +// relies on this to distinguish "override with an empty array" from +// "append zero table elements" (see below). static inline bool is_array_of_tables(toml_datum_t datum) { - bool ret = (datum.type == TOML_ARRAY); + bool ret = (datum.type == TOML_ARRAY) && (datum.u.arr.size > 0); for (int i = 0; ret && i < datum.u.arr.size; i++) { ret = (datum.u.arr.elem[i].type == TOML_TABLE); } diff --git a/test/merge/test1.c b/test/merge/test1.c index 3a1f806..d304981 100644 --- a/test/merge/test1.c +++ b/test/merge/test1.c @@ -166,6 +166,33 @@ static void test_array_of_tables_overridden_by_plain_array() { check(doc1, doc2, expected); } +// An array of tables overridden by an empty array must be replaced with +// the empty array, not left untouched. is_array_of_tables() used to treat +// an empty array as vacuously true (no elements fail the table check), so +// merging in an empty array hit the append branch instead of the override +// branch and appended zero elements onto the untouched destination. +static void test_array_of_tables_overridden_by_empty_array() { + printf("Running test_array_of_tables_overridden_by_empty_array...\n"); + const char *doc1 = "[[arr]]\n" + "x = 1"; + const char *doc2 = "arr = []"; + const char *expected = "arr = []"; + check(doc1, doc2, expected); +} + +// The reverse direction, for completeness: an empty array overridden by a +// real array of tables must end up with the array of tables, whether the +// merge takes the append path or the override path. +static void test_empty_array_overridden_by_array_of_tables() { + printf("Running test_empty_array_overridden_by_array_of_tables...\n"); + const char *doc1 = "arr = []"; + const char *doc2 = "[[arr]]\n" + "x = 1"; + const char *expected = "[[arr]]\n" + "x = 1"; + check(doc1, doc2, expected); +} + static void test_type_conflicts() { printf("Running test_type_conflicts...\n"); const char *doc1 = "value = 42"; @@ -217,6 +244,8 @@ int main() { test_array_of_tables(); test_plain_array_overridden_by_array_of_tables(); test_array_of_tables_overridden_by_plain_array(); + test_array_of_tables_overridden_by_empty_array(); + test_empty_array_overridden_by_array_of_tables(); test_type_conflicts(); test_empty_documents(); test_keys_outlive_inputs();