From 325b200d072680e2fc44951f7958b8d6053eca63 Mon Sep 17 00:00:00 2001 From: MarekZegare4 Date: Mon, 29 Jun 2026 18:13:05 +0200 Subject: [PATCH] fix(companion): compile-time tripwire for NodePrefs serialization drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NodePrefs is written/read field-by-field in DataStore::savePrefs/loadPrefsInt; the on-disk format is the struct's field layout, with nothing checking that the two hand-written sequences still match the struct. A forgotten read/write silently misaligns every later field — the highest-blast-radius convention in the codebase. Add a static_assert on sizeof(NodePrefs): changing a data member trips it with a checklist (sync save/load, clamp on load, bump SCHEMA_SENTINEL, update the size). A manual checkpoint, not a proof, but it turns silent drift into a build break. Size is variant-stable (all arrays sized by NodePrefs' own constants), verified on both nRF52 boards. Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/NodePrefs.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 45b131b6..09ba91c9 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -385,6 +385,23 @@ struct NodePrefs { // persisted to file } }; +// ── Serialization tripwire ─────────────────────────────────────────────────── +// NodePrefs is written/read field-by-field, in order, by DataStore::savePrefs() +// and loadPrefsInt(); the on-disk format IS the struct's field layout. There is +// no automatic check that those two hand-written sequences match the struct, so +// a forgotten read/write silently misaligns EVERY field after it. +// +// This assert is the manual checkpoint. Changing a data member changes sizeof +// and trips it. When it trips, do ALL of the following, then update the number: +// 1. add the field's rd(...) in DataStore::loadPrefsInt(), in struct order +// 2. add the field's write(...) in DataStore::savePrefs(), in struct order +// 3. clamp it on load (an upgrader's file lacks it → stray bytes) +// 4. bump SCHEMA_SENTINEL's low byte +// (Padding can also shift sizeof; a "false" trip just means re-check + rebump.) +static_assert(sizeof(NodePrefs) == 2488, + "NodePrefs layout changed — sync DataStore save/load + clamp, bump " + "SCHEMA_SENTINEL, then update this size (see steps above)."); + // Bounds for a usable repeater radio profile. The frequency range is passed in // by the caller from RadioLibWrapper::getFreqBounds() — the radio chip's own // validated range — so a profile that passes here is guaranteed to actually take