From d68cd2e2af3bb848981cc2692c26bd387541a8cd Mon Sep 17 00:00:00 2001 From: Rodion Kazennov Date: Thu, 29 Oct 2026 02:02:02 +0000 Subject: [PATCH] fix(diagram): set an empty PlantUML operand id when there is none dg_puml_operand set *label up front but *id only on its success paths, so an opener without its closer (`Alice ->: x`, `[-> Alice: x`, `class [Unclosed`) returned with *id unwritten. dg_puml_relation and dg_puml_decl then read an uninitialized dg_str_t and passed it to dg_element, which crashed the index worker on some stacks. The operand now starts with an empty id, so those lines take the existing `!lid.n` / `!rid.n` exits and declarations make no element. Fixes #2818 Signed-off-by: Rodion Kazennov --- internal/cbm/diagram.c | 4 +++- tests/test_diagrams.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/internal/cbm/diagram.c b/internal/cbm/diagram.c index cdd3785f4..23d5bf518 100644 --- a/internal/cbm/diagram.c +++ b/internal/cbm/diagram.c @@ -1225,9 +1225,11 @@ static bool dg_word_at(dg_str_t s, const char *word) { } /* An operand of a PlantUML line: `[Comp]`, `(use case)`, `:actor:`, `"text"`, - * an id, or `[*]`; its id in *id; the position after it (i when none). */ + * an id, or `[*]`; its id in *id; the position after it (i and an empty *id + * when none, as for the diagram edge in `A ->: x` or `[-> A: x`). */ static size_t dg_puml_operand(dg_str_t s, size_t i, dg_str_t *id, dg_str_t *label) { i = dg_skip_ws(s, i); + *id = dg_s(NULL, 0); *label = dg_s(NULL, 0); if (i + CBM_SZ_3 <= s.n && memcmp(s.p + i, "[*]", CBM_SZ_3) == 0) { *id = dg_s(s.p + i, CBM_SZ_3); diff --git a/tests/test_diagrams.c b/tests/test_diagrams.c index 574f27148..cf1cc6e7b 100644 --- a/tests/test_diagrams.c +++ b/tests/test_diagrams.c @@ -329,6 +329,36 @@ TEST(diagrams_plantuml) { PASS(); } +/* #2818: a sequence message to or from the diagram edge (`A ->: x`, `A <-: x`, + * `[-> A: x`) and a declaration with an unclosed bracket have no operand id; + * they used to read an uninitialized one and crash the index worker. */ +TEST(diagrams_plantuml_edge_messages) { + CBMFileResult *r = dg_file("@startuml\n" + "participant Alice\n" + "Alice ->: out\n" + "Alice <-: in\n" + "[-> Alice: from the edge\n" + "Alice -> Bob : hello\n" + "class [Unclosed\n" + "@enduml\n", + "docs/edge.puml"); + ASSERT_NOT_NULL(r); + ASSERT_NOT_NULL(dg_def(r, "edge#diagram-1.Alice")); + ASSERT_NOT_NULL(dg_def(r, "edge#diagram-1.Bob")); + ASSERT_EQ(r->diagram_arrows.count, 1); + ASSERT_EQ(dg_arrows(r, "#diagram-1.Alice", "#diagram-1.Bob", "message", "hello"), 1); + int elements = 0; + for (int i = 0; i < r->defs.count; i++) { + const char *label = r->defs.items[i].label; + if (label && strcmp(label, "DiagramElement") == 0) { + elements++; + } + } + ASSERT_EQ(elements, 2); + cbm_free_result(r); + PASS(); +} + TEST(diagrams_dot) { CBMFileResult *r = dg_file("digraph G {\n" @@ -1101,6 +1131,7 @@ SUITE(diagrams) { RUN_TEST(diagrams_mermaid_kinds); RUN_TEST(diagrams_other_fences); RUN_TEST(diagrams_plantuml); + RUN_TEST(diagrams_plantuml_edge_messages); RUN_TEST(diagrams_dot); RUN_TEST(diagrams_d2); RUN_TEST(diagrams_drawio);