From 2593a47fac5649a135216f4affd463abf29d4907 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 6 Jun 2026 13:43:19 +0000 Subject: [PATCH 1/4] feat: S14.09 per-message structured data via SolidSyslog_LogWithSd (#560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SolidSyslog_LogWithSd(handle, message, sd, sdCount) — attaches caller-built SD-ELEMENTs to a single Log call. SolidSyslog_Log becomes exactly LogWithSd(h, m, NULL, 0), so there is one formatting path. SolidSyslogMessage is unchanged (per-message SD is a call argument). MessageFormatter_Format gains the per-message array; FormatStructuredData emits the per-instance (config) SDs first, then the per-message array, into the one shared SolidSyslogSdElement, with NILVALUE only when neither produces an element (FormatSdElements helper DRYs the loop). A NULL array with nonzero count is reported (new SOLIDSYSLOG_ERROR_LOG_INCONSISTENT_SD, BAD_ARGUMENT) and treated as zero so the message still logs — mirroring the Create-time InstallStructuredData guard. Tests: emit, base-before-per-message ordering, empty-per-message keeps the base set, per-message values escaped (injection-proof, asserted on the raw frame), and the NULL-with-count guard. Worked example + oracle BDD + guide are S14.10. Co-Authored-By: Claude Opus 4.8 (1M context) --- Core/Interface/SolidSyslog.h | 14 +++++ Core/Interface/SolidSyslogErrors.h | 1 + Core/Source/SolidSyslog.c | 27 +++++++++- Core/Source/SolidSyslogMessageFormatter.c | 37 ++++++++++--- Core/Source/SolidSyslogMessageFormatter.h | 8 ++- Tests/SolidSyslogMessageFormatterTest.cpp | 2 +- Tests/SolidSyslogTest.cpp | 66 +++++++++++++++++++++++ 7 files changed, 143 insertions(+), 12 deletions(-) diff --git a/Core/Interface/SolidSyslog.h b/Core/Interface/SolidSyslog.h index 51753079..e30962f1 100644 --- a/Core/Interface/SolidSyslog.h +++ b/Core/Interface/SolidSyslog.h @@ -4,9 +4,12 @@ #include "ExternC.h" #include "SolidSyslogPrival.h" +#include + EXTERN_C_BEGIN struct SolidSyslog; + struct SolidSyslogStructuredData; struct SolidSyslogMessage { @@ -17,6 +20,17 @@ EXTERN_C_BEGIN }; void SolidSyslog_Log(struct SolidSyslog * handle, const struct SolidSyslogMessage* message); + + /* As SolidSyslog_Log, but attaches sd[0..sdCount) as caller-built SD-ELEMENTs to this one + message, emitted after the per-instance SDs registered at Create. The SD objects need only + live for the duration of the call. SolidSyslog_Log is exactly this with sd = NULL, count = 0. */ + void SolidSyslog_LogWithSd( + struct SolidSyslog * handle, + const struct SolidSyslogMessage* message, + struct SolidSyslogStructuredData** sd, + size_t sdCount + ); + void SolidSyslog_Service(struct SolidSyslog * handle); EXTERN_C_END diff --git a/Core/Interface/SolidSyslogErrors.h b/Core/Interface/SolidSyslogErrors.h index 5dc5e848..74e61a36 100644 --- a/Core/Interface/SolidSyslogErrors.h +++ b/Core/Interface/SolidSyslogErrors.h @@ -16,6 +16,7 @@ EXTERN_C_BEGIN SOLIDSYSLOG_ERROR_CREATE_INCONSISTENT_SD, SOLIDSYSLOG_ERROR_LOG_NULL_MESSAGE, SOLIDSYSLOG_ERROR_LOG_NULL_HANDLE, + SOLIDSYSLOG_ERROR_LOG_INCONSISTENT_SD, SOLIDSYSLOG_ERROR_SERVICE_NULL_HANDLE, SOLIDSYSLOG_ERROR_POOL_EXHAUSTED, SOLIDSYSLOG_ERROR_UNKNOWN_DESTROY, diff --git a/Core/Source/SolidSyslog.c b/Core/Source/SolidSyslog.c index b239eed6..854e076c 100644 --- a/Core/Source/SolidSyslog.c +++ b/Core/Source/SolidSyslog.c @@ -282,6 +282,16 @@ static inline void SolidSyslog_SendOneFromStore(struct SolidSyslog* self) } void SolidSyslog_Log(struct SolidSyslog* handle, const struct SolidSyslogMessage* message) +{ + SolidSyslog_LogWithSd(handle, message, NULL, 0); +} + +void SolidSyslog_LogWithSd( + struct SolidSyslog* handle, + const struct SolidSyslogMessage* message, + struct SolidSyslogStructuredData** sd, + size_t sdCount +) { if (handle == NULL) { @@ -301,10 +311,25 @@ void SolidSyslog_Log(struct SolidSyslog* handle, const struct SolidSyslogMessage } else { + /* Inconsistent pairing — the formatter would dereference sd[i] for + i < sdCount against a NULL array. Report and drop to no per-message + SD so the message still logs (degrade safely), mirroring the + Create-time guard in SolidSyslog_InstallStructuredData. */ + size_t safeSdCount = sdCount; + if ((sd == NULL) && (sdCount > 0U)) + { + SolidSyslog_Report( + SOLIDSYSLOG_BAD_ARGUMENT_SEVERITY, + SOLIDSYSLOG_CAT_BAD_ARGUMENT, + SOLIDSYSLOG_ERROR_LOG_INCONSISTENT_SD + ); + safeSdCount = 0U; + } + SolidSyslogFormatterStorage storage[SOLIDSYSLOG_FORMATTER_STORAGE_SIZE(SOLIDSYSLOG_MAX_MESSAGE_SIZE)]; struct SolidSyslogFormatter* f = SolidSyslogFormatter_Create(storage, SOLIDSYSLOG_MAX_MESSAGE_SIZE); - SolidSyslogMessageFormatter_Format(f, message, &handle->Format); + SolidSyslogMessageFormatter_Format(f, message, &handle->Format, sd, safeSdCount); SolidSyslogBuffer_Write( handle->Buffer, SolidSyslogFormatter_AsFormattedBuffer(f), diff --git a/Core/Source/SolidSyslogMessageFormatter.c b/Core/Source/SolidSyslogMessageFormatter.c index d71bff8e..8f5718e9 100644 --- a/Core/Source/SolidSyslogMessageFormatter.c +++ b/Core/Source/SolidSyslogMessageFormatter.c @@ -42,6 +42,13 @@ static inline void MessageFormatter_FormatMsgId(struct SolidSyslogFormatter* f, static inline bool MessageFormatter_StringIsValid(const char* value); static inline void MessageFormatter_FormatStructuredData( struct SolidSyslogFormatter* f, + struct SolidSyslogStructuredData** baseSd, + size_t baseSdCount, + struct SolidSyslogStructuredData** messageSd, + size_t messageSdCount +); +static inline void MessageFormatter_FormatSdElements( + struct SolidSyslogSdElement* element, struct SolidSyslogStructuredData** sd, size_t sdCount ); @@ -51,7 +58,9 @@ static inline const char* MessageFormatter_SkipLeadingBom(const char* msg); void SolidSyslogMessageFormatter_Format( struct SolidSyslogFormatter* f, const struct SolidSyslogMessage* message, - const struct SolidSyslogMessageFormatterContext* context + const struct SolidSyslogMessageFormatterContext* context, + struct SolidSyslogStructuredData** messageSd, + size_t messageSdCount ) { MessageFormatter_FormatPrival(f, MessageFormatter_MakePrival(message)); @@ -82,7 +91,7 @@ void SolidSyslogMessageFormatter_Format( SolidSyslogFormatter_AsciiCharacter(f, ' '); MessageFormatter_FormatMsgId(f, message->MessageId); SolidSyslogFormatter_AsciiCharacter(f, ' '); - MessageFormatter_FormatStructuredData(f, context->Sd, context->SdCount); + MessageFormatter_FormatStructuredData(f, context->Sd, context->SdCount, messageSd, messageSdCount); MessageFormatter_FormatMsg(f, message->Msg); } @@ -180,18 +189,18 @@ static inline bool MessageFormatter_StringIsValid(const char* value) static inline void MessageFormatter_FormatStructuredData( struct SolidSyslogFormatter* f, - struct SolidSyslogStructuredData** sd, - size_t sdCount + struct SolidSyslogStructuredData** baseSd, + size_t baseSdCount, + struct SolidSyslogStructuredData** messageSd, + size_t messageSdCount ) { size_t lengthBefore = SolidSyslogFormatter_Length(f); struct SolidSyslogSdElement element; SolidSyslogSdElement_FromFormatter(&element, f); - for (size_t i = 0; i < sdCount; i++) - { - SolidSyslogStructuredData_Format(sd[i], &element); - } + MessageFormatter_FormatSdElements(&element, baseSd, baseSdCount); + MessageFormatter_FormatSdElements(&element, messageSd, messageSdCount); if (SolidSyslogFormatter_Length(f) == lengthBefore) { @@ -199,6 +208,18 @@ static inline void MessageFormatter_FormatStructuredData( } } +static inline void MessageFormatter_FormatSdElements( + struct SolidSyslogSdElement* element, + struct SolidSyslogStructuredData** sd, + size_t sdCount +) +{ + for (size_t i = 0; i < sdCount; i++) + { + SolidSyslogStructuredData_Format(sd[i], element); + } +} + static inline void MessageFormatter_FormatMsg(struct SolidSyslogFormatter* f, const char* msg) { /* Guard msg before SkipLeadingBom dereferences it, then guard the diff --git a/Core/Source/SolidSyslogMessageFormatter.h b/Core/Source/SolidSyslogMessageFormatter.h index a1ff6e09..27308fe5 100644 --- a/Core/Source/SolidSyslogMessageFormatter.h +++ b/Core/Source/SolidSyslogMessageFormatter.h @@ -33,11 +33,15 @@ EXTERN_C_BEGIN /* Emits a full RFC 5424 SYSLOG-MSG into f: * 1 TIMESTAMP HOSTNAME APP-NAME PROCID MSGID SD [SP BOM MSG]. * Captures the timestamp via context->Clock and delegates its formatting - * to SolidSyslogTimestampFormatter_Format. */ + * to SolidSyslogTimestampFormatter_Format. The SD area emits the per-instance + * SDs (context->Sd) followed by the per-message SDs (messageSd[0..messageSdCount)), + * NILVALUE only when neither produces an element. */ void SolidSyslogMessageFormatter_Format( struct SolidSyslogFormatter * formatter, const struct SolidSyslogMessage* message, - const struct SolidSyslogMessageFormatterContext* context + const struct SolidSyslogMessageFormatterContext* context, + struct SolidSyslogStructuredData** messageSd, + size_t messageSdCount ); EXTERN_C_END diff --git a/Tests/SolidSyslogMessageFormatterTest.cpp b/Tests/SolidSyslogMessageFormatterTest.cpp index b40411fd..3e6bd5fa 100644 --- a/Tests/SolidSyslogMessageFormatterTest.cpp +++ b/Tests/SolidSyslogMessageFormatterTest.cpp @@ -56,7 +56,7 @@ TEST_GROUP(SolidSyslogMessageFormatter) void format() { - SolidSyslogMessageFormatter_Format(formatter, &message, &context); + SolidSyslogMessageFormatter_Format(formatter, &message, &context, nullptr, 0); } // Clears the output buffer between two format() calls without rebuilding diff --git a/Tests/SolidSyslogTest.cpp b/Tests/SolidSyslogTest.cpp index 499163c1..5bde4201 100644 --- a/Tests/SolidSyslogTest.cpp +++ b/Tests/SolidSyslogTest.cpp @@ -20,6 +20,7 @@ #include "SolidSyslogPassthroughBuffer.h" #include "SolidSyslogPrival.h" #include "SolidSyslogSdElement.h" +#include "SolidSyslogSdValue.h" #include "SolidSyslogStore.h" #include "SolidSyslogStructuredDataDefinition.h" #include "SolidSyslogTimeQuality.h" @@ -81,6 +82,15 @@ static void SdFailFormat(struct SolidSyslogStructuredData* /* self */, struct So static struct SolidSyslogStructuredData sdFail = {SdFailFormat}; +static void SdInjectFormat(struct SolidSyslogStructuredData* /* self */, struct SolidSyslogSdElement* element) +{ + SolidSyslogSdElement_Begin(element, "inj", 0U); + SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "k"), "a]b"); + SolidSyslogSdElement_End(element); +} + +static struct SolidSyslogStructuredData sdInject = {SdInjectFormat}; + static void IntegrationGetTimeQuality(struct SolidSyslogTimeQuality* timeQuality) { *timeQuality = {true, true, SOLIDSYSLOG_SYNC_ACCURACY_OMIT}; @@ -226,6 +236,47 @@ TEST(SolidSyslog, InjectedSdObjectFormatIsCalledDuringLog) STRCMP_EQUAL("[spy]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); } +TEST(SolidSyslog, LogWithSdEmitsThePerMessageElement) +{ + SolidSyslogStructuredData* perMessage[] = {&sdSpy}; + SolidSyslog_LogWithSd(solidSyslog, &message, perMessage, 1); + STRCMP_EQUAL("[spy]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); +} + +TEST(SolidSyslog, LogWithSdEmitsPerInstanceBeforePerMessage) +{ + SolidSyslogStructuredData* baseList[] = {&sdSpy}; + config.Sd = baseList; + config.SdCount = 1; + SolidSyslog_Destroy(solidSyslog); + solidSyslog = SolidSyslog_Create(&config); + + SolidSyslogStructuredData* perMessage[] = {&sdSpy2}; + SolidSyslog_LogWithSd(solidSyslog, &message, perMessage, 1); + STRCMP_EQUAL("[spy][spy2]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); +} + +TEST(SolidSyslog, LogWithSdEmptyPerMessageKeepsPerInstanceSd) +{ + SolidSyslogStructuredData* baseList[] = {&sdSpy}; + config.Sd = baseList; + config.SdCount = 1; + SolidSyslog_Destroy(solidSyslog); + solidSyslog = SolidSyslog_Create(&config); + + SolidSyslog_LogWithSd(solidSyslog, &message, nullptr, 0); + STRCMP_EQUAL("[spy]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); +} + +TEST(SolidSyslog, LogWithSdEscapesPerMessageValues) +{ + SolidSyslogStructuredData* perMessage[] = {&sdInject}; + SolidSyslog_LogWithSd(solidSyslog, &message, perMessage, 1); + /* The injected ']' is escaped to '\]' so it cannot break the SD framing + (asserted against the raw frame — the SDATA helper stops at any ']'). */ + STRCMP_CONTAINS("[inj k=\"a\\]b\"]", lastMessage()); +} + TEST(SolidSyslog, MetaSdProducesSequenceIdInStructuredData) { metaSdCounter = TestAtomicCounter_Create(); @@ -889,6 +940,21 @@ TEST(SolidSyslogLifecycle, LogWithNullMessageReportsError) UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_ERROR_LOG_NULL_MESSAGE, ErrorHandlerFake_LastDetail()); } +TEST(SolidSyslogLifecycle, LogWithSdNullArrayAndNonZeroCountReportsError) +{ + SolidSyslogConfig config = validConfig(); + solidSyslog = SolidSyslog_Create(&config); + ErrorHandlerFake_Install(nullptr); + + SolidSyslog_LogWithSd(solidSyslog, &message, nullptr, 1); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_CRITICAL, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&SolidSyslogErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_CAT_BAD_ARGUMENT, ErrorHandlerFake_LastCategory()); + UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_ERROR_LOG_INCONSISTENT_SD, ErrorHandlerFake_LastDetail()); +} + TEST(SolidSyslogLifecycle, CreateWithNullConfigReportsError) { ErrorHandlerFake_Install(nullptr); From 0a519e1c70b12f0d052870ef1d6cb1f6f09b4499 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 6 Jun 2026 13:59:32 +0000 Subject: [PATCH 2/4] refactor: S14.09 share Log/LogWithSd body via static SolidSyslog_DoLog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Having SolidSyslog_Log call SolidSyslog_LogWithSd gave LogWithSd exactly one internal reference, which cppcheck-misra flags as 8.7 (external linkage used in one TU) — the public API is genuinely called only from integrator code the analysis set can't see. Rather than add the project's first active 8.7 suppression (D.014 retired the last), both public functions now delegate to a static SolidSyslog_DoLog holding the shared validation/guard/format/buffer body, matching the file's existing thin-public-over-static-helper pattern. Named DoLog (not Emit) — 'emit' is already taken in OriginSd for writing one SD piece and is heavily used in prose, while _Do is the established 'real impl behind the public op' idiom (LwipRawTcpStream_Do*). Also: CLAUDE.md SolidSyslog.h row documents LogWithSd; misra_suppressions.txt 11.8 line drift renumbered (SolidSyslog.c +6, MessageFormatter.c +9), verified exit 0 against CI's --addon=misra command. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- Core/Source/SolidSyslog.c | 18 +++++++++++++++++- misra_suppressions.txt | 18 +++++++++--------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index aadf799f..5fa42301 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -315,7 +315,7 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*` | Header | Audience | Provides | |---|---|---| -| `SolidSyslog.h` | Application code that logs events | `SolidSyslogMessage`, `SolidSyslog_Log(handle, message)`, `SolidSyslog_Service(handle)`. `struct SolidSyslog` opaque. | +| `SolidSyslog.h` | Application code that logs events | `SolidSyslogMessage`, `SolidSyslog_Log(handle, message)`, `SolidSyslog_LogWithSd(handle, message, sd, sdCount)` (attach caller-built per-message SD-ELEMENTs after the per-instance SDs; `Log` is exactly this with `NULL, 0`), `SolidSyslog_Service(handle)`. `struct SolidSyslog` opaque. | | `SolidSyslogConfig.h` | System setup code | `SolidSyslogConfig`, `SolidSyslog_Create(config) → handle`, `SolidSyslog_Destroy(handle)`. Instance struct lives in a library-internal static pool (E11; tunable `SOLIDSYSLOG_POOL_SIZE`, default `1U`). Pool-exhaustion fallback is a TU-private `NullInstance` wired to `SolidSyslogNull{Buffer,Sender,Store}_Get()` siblings — `_Log`/`_Service` against it dispatch through Null vtables and silently drop. | | `SolidSyslogErrors.h` | Any code installing an error handler that wants to react to SolidSyslog-singleton events (pointer-identity match on `SolidSyslogErrorSource`, switch on `enum SolidSyslogErrors`) | `enum SolidSyslogErrors` (`SOLIDSYSLOG_ERROR_*` codes + `SOLIDSYSLOG_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource SolidSyslogErrorSource`. Integrators ignore if not handling errors per source. | | `SolidSyslogError.h` | Any code installing a handler to react to library-internal errors (NULL guards, send failures); also the library-internal call site for emitting them | `struct SolidSyslogErrorEvent` (`Severity`, `Source`, `uint16_t Category`, `int32_t Detail`), `SolidSyslogErrorHandler` typedef (`void(void* context, const struct SolidSyslogErrorEvent* event)`), `SolidSyslog_SetErrorHandler(handler, context)`, `SolidSyslog_Error(severity, source, category, detail)`. The handler reads three orthogonal axes off the event: `Source` (extensible identity, pointer-identity match), `Category` (portable reaction axis — see `SolidSyslogErrorCategory.h`), `Detail` (per-class `enum SolidSyslogErrors` value today, native `errno`/`X509_V_ERR_*` later). Default handler is a silent no-op; setting `handler = NULL` restores the default. Single global slot — intended for setup-time configuration, not synchronised with concurrent `Error` calls. | diff --git a/Core/Source/SolidSyslog.c b/Core/Source/SolidSyslog.c index 854e076c..e054dce6 100644 --- a/Core/Source/SolidSyslog.c +++ b/Core/Source/SolidSyslog.c @@ -31,6 +31,12 @@ struct SolidSyslogStructuredData; static inline void SolidSyslog_DrainBufferIntoStore(struct SolidSyslog* self); static inline void SolidSyslog_SendOneFromStore(struct SolidSyslog* self); +static void SolidSyslog_DoLog( + struct SolidSyslog* handle, + const struct SolidSyslogMessage* message, + struct SolidSyslogStructuredData** sd, + size_t sdCount +); static void SolidSyslog_InstallAppName( struct SolidSyslog* self, SolidSyslogHeaderFieldFunction configured, @@ -283,7 +289,7 @@ static inline void SolidSyslog_SendOneFromStore(struct SolidSyslog* self) void SolidSyslog_Log(struct SolidSyslog* handle, const struct SolidSyslogMessage* message) { - SolidSyslog_LogWithSd(handle, message, NULL, 0); + SolidSyslog_DoLog(handle, message, NULL, 0); } void SolidSyslog_LogWithSd( @@ -292,6 +298,16 @@ void SolidSyslog_LogWithSd( struct SolidSyslogStructuredData** sd, size_t sdCount ) +{ + SolidSyslog_DoLog(handle, message, sd, sdCount); +} + +static void SolidSyslog_DoLog( + struct SolidSyslog* handle, + const struct SolidSyslogMessage* message, + struct SolidSyslogStructuredData** sd, + size_t sdCount +) { if (handle == NULL) { diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 5d36ba1d..2478a9ce 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -103,19 +103,19 @@ misra-c2012-18.7:Core/Source/SolidSyslogFormatter.c:12 # D.006 — Rule 11.8: const-strip false-positives + platform-API (Winsock, lwIP) # See docs/misra-deviations.md#d006 misra-c2012-11.8:Core/Source/BlockSequence.c:465 -misra-c2012-11.8:Core/Source/SolidSyslog.c:65 -misra-c2012-11.8:Core/Source/SolidSyslog.c:66 -misra-c2012-11.8:Core/Source/SolidSyslog.c:67 -misra-c2012-11.8:Core/Source/SolidSyslog.c:68 -misra-c2012-11.8:Core/Source/SolidSyslog.c:69 -misra-c2012-11.8:Core/Source/SolidSyslog.c:70 misra-c2012-11.8:Core/Source/SolidSyslog.c:71 misra-c2012-11.8:Core/Source/SolidSyslog.c:72 -misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:60 -misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:62 +misra-c2012-11.8:Core/Source/SolidSyslog.c:73 +misra-c2012-11.8:Core/Source/SolidSyslog.c:74 +misra-c2012-11.8:Core/Source/SolidSyslog.c:75 +misra-c2012-11.8:Core/Source/SolidSyslog.c:76 +misra-c2012-11.8:Core/Source/SolidSyslog.c:77 +misra-c2012-11.8:Core/Source/SolidSyslog.c:78 misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:69 -misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:76 +misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:71 +misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:78 misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:85 +misra-c2012-11.8:Core/Source/SolidSyslogMessageFormatter.c:94 misra-c2012-11.8:Core/Source/SolidSyslogBlockStoreStatic.c:46 misra-c2012-11.8:Core/Source/SolidSyslogBlockStoreStatic.c:117 misra-c2012-11.8:Core/Source/SolidSyslogBlockStoreStatic.c:143 From 6e8238f312e275e213d25d12c9e98edc853b1511 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 6 Jun 2026 14:00:09 +0000 Subject: [PATCH 3/4] docs: S14.09 DEVLOG entry --- DEVLOG.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/DEVLOG.md b/DEVLOG.md index 1c7d6889..94884572 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -14657,3 +14657,51 @@ MISRA rule — different category, doesn't set precedent. ### Open questions - None outstanding. + +## 2026-06-06 — S14.09: per-message structured data (SolidSyslog_LogWithSd) + +This is the per-message-attachment wave of E14 — the headline "custom SD on +individual Log calls" — pulled ahead of the integrator guide (the old S14.09 +guide was renumbered to **S14.10 #549**; this story is the new **#560**). Confirmed +the epic still scopes it ("attaching caller-built SD-ELEMENTs … via +SolidSyslogMessage"; the per-message Open questions); it was the never-decomposed +"Later wave". + +### Decisions +- **`SolidSyslog_LogWithSd(handle, message, sd, sdCount)`** — David's shape: per-message + SD is a *call argument* (array of `SolidSyslogStructuredData*`, same type as + `SolidSyslogConfig.Sd[]`), not a field on `SolidSyslogMessage` (which stays a pure + data struct). `SolidSyslog_Log` is exactly `LogWithSd(h, m, NULL, 0)`. +- **Ordering:** per-instance (config) SDs first, then per-message — both into the one + shared `SolidSyslogSdElement` the dispatch already builds; NILVALUE only when neither + writes. `MessageFormatter_Format` gained the per-message array; `FormatSdElements` + helper DRYs the base-then-message loop. +- **NULL array + nonzero count** → reported (new `SOLIDSYSLOG_ERROR_LOG_INCONSISTENT_SD`, + BAD_ARGUMENT) and treated as 0 so the message still logs — mirrors the Create-time + `InstallStructuredData` guard. +- **Reentrancy clarification (David pushed back, rightly):** the vtable `Format(self, + element)` has no per-call argument, so a custom SD reads call-specific data via `self`. + Formatting is synchronous, so the SD objects need only live across the call. The only + hazard is sharing one mutable SD object across concurrent Log calls — avoidable with a + per-call-site instance. Not an API flaw; a one-line note for the S14.10 guide. +- **MISRA 8.7 surprise + the `_Emit`→`_DoLog` naming:** making `Log` call `LogWithSd` + directly gave `LogWithSd` exactly one internal reference → cppcheck 8.7 (public API, + external callers invisible to the analysis set; a BDD/test caller wouldn't help — those + aren't in the cppcheck file set). Rather than add the project's first active 8.7 + suppression (D.014 retired the last), both public functions delegate to a static + `SolidSyslog_DoLog`, so each is zero-internal-ref (like every other public function + here). Named DoLog not Emit — `_Emit` is taken in OriginSd (write one SD piece) and + heavy in prose; `_Do` is the established "real impl behind the public op" + idiom (`LwipRawTcpStream_Do*`). +- Tests: emit, base-before-per-message ordering, empty-per-message keeps the base set, + per-message values escaped (injection-proof — asserted on the raw frame because the + SDATA test-helper naively stops at any `]`), NULL-with-count guard. 1479 green; + BddTargetTests 66/66; clang-format clean; cppcheck-MISRA exit 0 (8.7 cleared by the + static helper; 11.8 line drift renumbered SolidSyslog.c +6 / MessageFormatter.c +9). + +### Deferred +- Worked custom-SD example, oracle round-trip BDD, and the integrator-guide prose + (incl. the don't-share-a-mutable-SD-object note) → **S14.10 (#549)**, which closes E14. + +### Open questions +- None outstanding. From afceb4f2e85fcf643d748d10508b791e0c5f7dc1 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 6 Jun 2026 14:10:05 +0000 Subject: [PATCH 4/4] feat: S14.09 skip NULL SD entries instead of crashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A NULL element inside an Sd array (per-message or config) dereferenced through the unguarded SolidSyslogStructuredData_Format dispatch and crashed; nothing tested it (create-time relied only on the SolidSyslogNullSd convention). Guard the shared MessageFormatter_FormatSdElements loop so a conditionally-absent (NULL) SD is skipped and the message still emits — the per-message array is caller input at the call site and must not crash the library. Tests: NULL element skipped in both the per-message and config arrays, plus NULL-array-with- count still emits NILVALUE without crashing (the emit side of the existing report-only guard test). Co-Authored-By: Claude Opus 4.8 (1M context) --- Core/Source/SolidSyslogMessageFormatter.c | 9 ++++++++- DEVLOG.md | 7 +++++++ Tests/SolidSyslogTest.cpp | 24 +++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Core/Source/SolidSyslogMessageFormatter.c b/Core/Source/SolidSyslogMessageFormatter.c index 8f5718e9..9068aed7 100644 --- a/Core/Source/SolidSyslogMessageFormatter.c +++ b/Core/Source/SolidSyslogMessageFormatter.c @@ -216,7 +216,14 @@ static inline void MessageFormatter_FormatSdElements( { for (size_t i = 0; i < sdCount; i++) { - SolidSyslogStructuredData_Format(sd[i], element); + /* Skip NULL entries rather than dereference them. Per-instance slots are + expected to use SolidSyslogNullSd, but a per-message array is supplied + at the call site where a conditionally-absent SD is naturally NULL — + the library must not crash on caller input. */ + if (sd[i] != NULL) + { + SolidSyslogStructuredData_Format(sd[i], element); + } } } diff --git a/DEVLOG.md b/DEVLOG.md index 94884572..ced6197b 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -14679,6 +14679,13 @@ SolidSyslogMessage"; the per-message Open questions); it was the never-decompose - **NULL array + nonzero count** → reported (new `SOLIDSYSLOG_ERROR_LOG_INCONSISTENT_SD`, BAD_ARGUMENT) and treated as 0 so the message still logs — mirrors the Create-time `InstallStructuredData` guard. +- **NULL element *inside* the array** (David's catch on review) → skipped, message still + emits, no crash. The dispatch `SolidSyslogStructuredData_Format` never null-guarded, and + nothing tested a NULL element in *either* array (create-time relied only on the + "use SolidSyslogNullSd, not NULL" convention). Guard added in the shared + `MessageFormatter_FormatSdElements` loop, so both the per-instance and per-message arrays + now tolerate a conditionally-absent (NULL) SD — justified because the per-message array is + caller input at the call site. Tests added for both arrays + the still-emits-NILVALUE case. - **Reentrancy clarification (David pushed back, rightly):** the vtable `Format(self, element)` has no per-call argument, so a custom SD reads call-specific data via `self`. Formatting is synchronous, so the SD objects need only live across the call. The only diff --git a/Tests/SolidSyslogTest.cpp b/Tests/SolidSyslogTest.cpp index 5bde4201..a27ac84f 100644 --- a/Tests/SolidSyslogTest.cpp +++ b/Tests/SolidSyslogTest.cpp @@ -236,6 +236,17 @@ TEST(SolidSyslog, InjectedSdObjectFormatIsCalledDuringLog) STRCMP_EQUAL("[spy]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); } +TEST(SolidSyslog, NullElementInConfigSdArrayIsSkipped) +{ + SolidSyslogStructuredData* sdList[] = {&sdSpy, nullptr}; + config.Sd = sdList; + config.SdCount = 2; + SolidSyslog_Destroy(solidSyslog); + solidSyslog = SolidSyslog_Create(&config); + Log(); + STRCMP_EQUAL("[spy]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); +} + TEST(SolidSyslog, LogWithSdEmitsThePerMessageElement) { SolidSyslogStructuredData* perMessage[] = {&sdSpy}; @@ -256,6 +267,19 @@ TEST(SolidSyslog, LogWithSdEmitsPerInstanceBeforePerMessage) STRCMP_EQUAL("[spy][spy2]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); } +TEST(SolidSyslog, LogWithSdSkipsNullElementsInTheArray) +{ + SolidSyslogStructuredData* perMessage[] = {&sdSpy, nullptr, &sdSpy2}; + SolidSyslog_LogWithSd(solidSyslog, &message, perMessage, 3); + STRCMP_EQUAL("[spy][spy2]", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); +} + +TEST(SolidSyslog, LogWithSdNullArrayWithCountStillEmitsNilvalue) +{ + SolidSyslog_LogWithSd(solidSyslog, &message, nullptr, 1); + STRCMP_EQUAL("-", SyslogField(lastMessage(), SYSLOG_FIELD_SDATA).c_str()); +} + TEST(SolidSyslog, LogWithSdEmptyPerMessageKeepsPerInstanceSd) { SolidSyslogStructuredData* baseList[] = {&sdSpy};