Files
ZephCore/zephcore/helpers/PrefsCodec.cpp
T
ZephCore T096andClaude Sonnet 5 f8a4992ff0 prefs: encode led_brightness too, not just decode; fix pinned test layout
companionPrefsEncode()/serverPrefsEncode() never wrote the led_brightness
byte that companionPrefsDecode()/serverPrefsDecode() already read,
desyncing every field that follows it by one byte on the companion side
(wifi_enabled/wifi_ssid/wifi_pwd) and silently truncating the server
format by one byte. Caught by CI's ASan/UBSan run on the host regression
suite: an out-of-bounds read/write in the pinned layout tests.

Since led_brightness never existed upstream, nothing already accounted
for it: fixing only the encoder still leaves the pinned byte tables and
the documented 272/311-byte totals (COMPANION_PREFS_SIZE/
SERVER_PREFS_SIZE, used by the real prefs stores too, not just tests)
one byte short of what the encoder now actually produces.

- PrefsCodec.h: bump both size constants (272->273, 311->312).
- PrefsCodec.cpp: add the missing w.put() in both encoders, mirroring
  the position already used by both decoders.
- prefs_codec_tests.cpp: add led_brightness to both layout tables at its
  real file offset (shifts wifi_enabled/wifi_ssid/wifi_pwd by one byte
  on the companion side; server's is appended last, nothing else
  shifts), extend both historical-boundary length lists with the new
  full-format length, and give it a non-default value in sample() so
  the layout/roundtrip tests actually exercise it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 17:38:54 +02:00

389 lines
15 KiB
C++

/*
* SPDX-License-Identifier: MIT
* PrefsCodec - byte codecs for the two binary prefs files. See PrefsCodec.h.
*/
#include "PrefsCodec.h"
#include <string.h>
namespace {
/* Sequential writer / reader over a byte buffer. Rd::get() mirrors fs_read()
* on a short file, as the server store used to read it: a field that straddles
* the end gets the bytes that exist. Rd::take() reads whole fields only. Past
* the end, both leave the field as it was. */
struct Wr {
uint8_t *b;
size_t off;
void put(const void *src, size_t n) { memcpy(b + off, src, n); off += n; }
void zeros(size_t n) { memset(b + off, 0, n); off += n; }
};
struct Rd {
const uint8_t *b;
size_t len;
size_t off;
void get(void *dst, size_t n) {
size_t k = off < len ? (len - off < n ? len - off : n) : 0;
memcpy(dst, b + off, k);
off += n;
}
void skip(size_t n) { off += n; }
/* Whole fields only: one that does not fit keeps its value. */
void take(void *dst, size_t n) {
if (off + n <= len) {
memcpy(dst, b + off, n);
}
off += n;
}
};
} // namespace
/* ── Companion: /lfs/new_prefs ─────────────────────────────────────── */
size_t companionPrefsEncode(const NodePrefs &p, uint8_t *buf, size_t cap)
{
if (cap < COMPANION_PREFS_SIZE) {
return 0;
}
Wr w = { buf, 0 };
/* 0-91: the layout upstream's new_prefs had */
w.put(&p.airtime_factor, 4);
w.put(p.node_name, 32);
w.zeros(4);
w.put(&p.node_lat, 8);
w.put(&p.node_lon, 8);
w.put(&p.freq, 4);
w.put(&p.sf, 1);
w.put(&p.cr, 1);
w.put(&p.client_repeat, 1);
w.put(&p.manual_add_contacts, 1);
w.put(&p.bw, 4);
w.put(&p.tx_power_dbm, 1);
w.put(&p.telemetry_mode_base, 1);
w.put(&p.telemetry_mode_loc, 1);
w.put(&p.telemetry_mode_env, 1);
w.put(&p.rx_delay_base, 4);
w.put(&p.advert_loc_policy, 1);
w.put(&p.multi_acks, 1);
w.put(&p.path_hash_mode, 1);
w.zeros(1);
w.put(&p.ble_pin, 4);
w.put(&p.buzzer_quiet, 1);
w.put(&p.gps_enabled, 1);
w.put(&p.gps_interval, 4);
w.put(&p.autoadd_config, 1);
w.put(&p.autoadd_max_hops, 1);
/* 92-: ZephCore. Upstream put default_scope_name at 93. */
w.put(&p.rx_boost, 1);
w.put(&p.leds_disabled, 1);
w.put(&p._reserved_apc_enabled, 1); /* 94-95: APC, removed in 1.16.6 */
w.put(&p._reserved_apc_margin, 1);
w.put(p.default_scope_name, 31);
w.put(p.default_scope_key, 16);
w.put(&p.ble_disabled, 1);
w.put(&p.display_brightness, 1);
w.put(&p.wake_on_msg, 1);
w.put(&p.screen_off_secs, 2);
w.put(&p.auto_shutdown_mv, 2);
w.put(&p.rx_duty_cycle, 1);
w.put(&p.meshtimesync, 1);
w.put(&p.v_contact_enabled, 1);
w.put(&p.v_battery_alert_mv, 2);
w.put(&p.cad_auto, 1);
w.put(&p.cad_offset, 1);
w.put(&p.probe_interval, 1);
w.put(&p.cad_busycap, 1);
w.put(&p.adc_multiplier, 4);
w.put(p.extra_sf, EXTRA_SF_MAX);
w.put(&p.v_contact_flags, 1);
w.put(&p.fem_rxgain, 1);
w.put(&p.display_rotate, 1);
w.put(&p.input_rotate, 1);
w.put(&p.cad_base, 1);
w.put(&p.tz_offset, 1);
w.put(&p.leds_radio_mode, 1);
w.put(&p.leds_hb_mode, 1);
w.put(&p.led_brightness, 1);
w.put(&p.wifi_enabled, 1);
w.put(p.wifi_ssid, sizeof(p.wifi_ssid));
w.put(p.wifi_pwd, sizeof(p.wifi_pwd));
return w.off;
}
bool companionPrefsDecode(NodePrefs &prefs, const uint8_t *buf, size_t len)
{
/* gps_interval ends at 90: anything shorter is not a prefs file */
if (len < 90) {
return false;
}
/* Committed only if the radio preset is plausible: a file in some other
* layout must not become our radio settings. */
NodePrefs p = prefs;
Rd r = { buf, len, 0 };
r.take(&p.airtime_factor, 4);
r.take(p.node_name, 32);
r.skip(4);
r.take(&p.node_lat, 8);
r.take(&p.node_lon, 8);
r.take(&p.freq, 4);
r.take(&p.sf, 1);
r.take(&p.cr, 1);
r.take(&p.client_repeat, 1);
r.take(&p.manual_add_contacts, 1);
r.take(&p.bw, 4);
if (p.freq < ZC_RADIO_FREQ_MIN_MHZ || p.freq > ZC_RADIO_FREQ_MAX_MHZ ||
p.sf < 5 || p.sf > 12 ||
p.bw < ZC_RADIO_BW_MIN_KHZ || p.bw > ZC_RADIO_BW_MAX_KHZ) {
return false;
}
r.take(&p.tx_power_dbm, 1);
r.take(&p.telemetry_mode_base, 1);
r.take(&p.telemetry_mode_loc, 1);
r.take(&p.telemetry_mode_env, 1);
r.take(&p.rx_delay_base, 4);
r.take(&p.advert_loc_policy, 1);
r.take(&p.multi_acks, 1);
r.take(&p.path_hash_mode, 1);
r.skip(1);
r.take(&p.ble_pin, 4);
r.take(&p.buzzer_quiet, 1);
r.take(&p.gps_enabled, 1);
r.take(&p.gps_interval, 4);
r.take(&p.autoadd_config, 1);
r.take(&p.autoadd_max_hops, 1);
/* Each field below is newer than some deployed files: one past the end
* keeps the caller's initNodePrefs() default. */
r.take(&p.rx_boost, 1);
r.take(&p.leds_disabled, 1);
r.take(&p._reserved_apc_enabled, 1);
r.take(&p._reserved_apc_margin, 1);
r.take(p.default_scope_name, 31);
r.take(p.default_scope_key, 16);
r.take(&p.ble_disabled, 1);
r.take(&p.display_brightness, 1);
r.take(&p.wake_on_msg, 1);
r.take(&p.screen_off_secs, 2);
r.take(&p.auto_shutdown_mv, 2);
r.take(&p.rx_duty_cycle, 1);
r.take(&p.meshtimesync, 1);
r.take(&p.v_contact_enabled, 1);
r.take(&p.v_battery_alert_mv, 2);
r.take(&p.cad_auto, 1);
r.take(&p.cad_offset, 1);
r.take(&p.probe_interval, 1);
r.take(&p.cad_busycap, 1);
r.take(&p.adc_multiplier, 4);
r.take(p.extra_sf, EXTRA_SF_MAX);
r.take(&p.v_contact_flags, 1);
r.take(&p.fem_rxgain, 1);
r.take(&p.display_rotate, 1);
r.take(&p.input_rotate, 1);
/* cad_base 0 = "no base recorded": setCadParams() then leaves the stored
* offset alone. */
r.take(&p.cad_base, 1);
r.take(&p.tz_offset, 1);
r.take(&p.leds_radio_mode, 1);
r.take(&p.leds_hb_mode, 1);
/* led_brightness (ZephCore extension, since 1.17.4). Absent in files
* written before this field existed; the no-op EOF read leaves the
* initNodePrefs() default ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT (10%),
* same value a fresh node already showed before this was persisted. */
r.take(&p.led_brightness, 1);
r.take(&p.wifi_enabled, 1);
r.take(p.wifi_ssid, sizeof(p.wifi_ssid));
r.take(p.wifi_pwd, sizeof(p.wifi_pwd));
/* The one validator: bounds, NaNs, and the unterminated char fields */
sanitizeNodePrefs(&p);
prefs = p;
return true;
}
/* ── Server roles: /lfs/repeater/prefs ─────────────────────────────── */
size_t serverPrefsEncode(const NodePrefs &prefs, uint8_t *buf, size_t cap)
{
if (cap < SERVER_PREFS_SIZE) {
return 0;
}
Wr w = { buf, 0 };
/* Same order as Arduino CommonCLI up to offset 290 */
w.put(&prefs.airtime_factor, sizeof(prefs.airtime_factor));
w.put(&prefs.node_name, sizeof(prefs.node_name));
w.zeros(4);
w.put(&prefs.node_lat, sizeof(prefs.node_lat));
w.put(&prefs.node_lon, sizeof(prefs.node_lon));
w.put(&prefs.password, sizeof(prefs.password));
w.put(&prefs.freq, sizeof(prefs.freq));
w.put(&prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm));
w.put(&prefs.disable_fwd, sizeof(prefs.disable_fwd));
w.put(&prefs.advert_interval, sizeof(prefs.advert_interval));
w.zeros(1);
w.put(&prefs.rx_delay_base, sizeof(prefs.rx_delay_base));
w.put(&prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor));
w.put(&prefs.guest_password, sizeof(prefs.guest_password));
w.put(&prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor));
w.put(&prefs.backoff_multiplier, sizeof(prefs.backoff_multiplier));
w.put(&prefs.sf, sizeof(prefs.sf));
w.put(&prefs.cr, sizeof(prefs.cr));
w.put(&prefs.allow_read_only, sizeof(prefs.allow_read_only));
w.put(&prefs.multi_acks, sizeof(prefs.multi_acks));
w.put(&prefs.bw, sizeof(prefs.bw));
/* 120: leds_disabled, magic-encoded (was agc_reset_interval). */
{
uint8_t leds_byte = prefs.leds_disabled ? LEDS_PREF_OFF : LEDS_PREF_ON;
w.put(&leds_byte, sizeof(leds_byte));
}
w.put(&prefs.path_hash_mode, sizeof(prefs.path_hash_mode));
w.put(&prefs.loop_detect, sizeof(prefs.loop_detect));
w.zeros(1);
w.put(&prefs.flood_max, sizeof(prefs.flood_max));
w.put(&prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval));
w.put(&prefs.interference_threshold, sizeof(prefs.interference_threshold));
w.zeros(25); // bridge settings
w.put(&prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled));
w.zeros(3);
w.put(&prefs.gps_enabled, sizeof(prefs.gps_enabled));
w.put(&prefs.gps_interval, sizeof(prefs.gps_interval));
w.put(&prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy));
w.put(&prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp));
w.put(&prefs.adc_multiplier, sizeof(prefs.adc_multiplier));
w.put(prefs.owner_info, sizeof(prefs.owner_info));
/* ZephCore extensions */
w.put(&prefs.rx_boost, sizeof(prefs.rx_boost));
w.put(&prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
/* RESERVED — formerly apc_enabled / apc_margin (removed in 1.16.6).
* Written back unchanged to hold the layout. */
w.put(&prefs._reserved_apc_enabled, sizeof(prefs._reserved_apc_enabled));
w.put(&prefs._reserved_apc_margin, sizeof(prefs._reserved_apc_margin));
/* Flood hop-ceiling extensions (offsets 294-295) */
w.put(&prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped));
w.put(&prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
/* Mesh time sync on/off (offset 296) */
w.put(&prefs.meshtimesync, sizeof(prefs.meshtimesync));
/* Adaptive CAD (offsets 297-300) */
w.put(&prefs.cad_auto, sizeof(prefs.cad_auto));
w.put(&prefs.cad_offset, sizeof(prefs.cad_offset));
w.put(&prefs.probe_interval, sizeof(prefs.probe_interval));
w.put(&prefs.cad_busycap, sizeof(prefs.cad_busycap));
/* LR2021 side-detector SFs (offsets 301-303) */
w.put(prefs.extra_sf, sizeof(prefs.extra_sf));
/* External FEM RX gain (offset 304) */
w.put(&prefs.fem_rxgain, sizeof(prefs.fem_rxgain));
/* Mounting orientation (offsets 305-306) */
w.put(&prefs.display_rotate, sizeof(prefs.display_rotate));
w.put(&prefs.input_rotate, sizeof(prefs.input_rotate));
/* Family base detPeak cad_offset was learned against (offset 307) */
w.put(&prefs.cad_base, sizeof(prefs.cad_base));
/* Display timezone offset (offset 308) — signed whole hours from UTC,
* applied only when formatting the on-device clock */
w.put(&prefs.tz_offset, sizeof(prefs.tz_offset));
/* LED activity/heartbeat modes (offsets 309-310) */
w.put(&prefs.leds_radio_mode, sizeof(prefs.leds_radio_mode));
w.put(&prefs.leds_hb_mode, sizeof(prefs.leds_hb_mode));
/* led_brightness (ZephCore extension, since 1.17.4), offset 311. */
w.put(&prefs.led_brightness, sizeof(prefs.led_brightness));
return w.off;
}
void serverPrefsDecode(NodePrefs &prefs, const uint8_t *buf, size_t len)
{
Rd r = { buf, len, 0 };
/* Same order as Arduino CommonCLI up to offset 290 */
r.get(&prefs.airtime_factor, sizeof(prefs.airtime_factor));
r.get(&prefs.node_name, sizeof(prefs.node_name));
r.skip(4);
r.get(&prefs.node_lat, sizeof(prefs.node_lat));
r.get(&prefs.node_lon, sizeof(prefs.node_lon));
r.get(&prefs.password, sizeof(prefs.password));
r.get(&prefs.freq, sizeof(prefs.freq));
r.get(&prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm));
r.get(&prefs.disable_fwd, sizeof(prefs.disable_fwd));
r.get(&prefs.advert_interval, sizeof(prefs.advert_interval));
r.skip(1);
r.get(&prefs.rx_delay_base, sizeof(prefs.rx_delay_base));
r.get(&prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor));
r.get(&prefs.guest_password, sizeof(prefs.guest_password));
r.get(&prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor));
r.get(&prefs.backoff_multiplier, sizeof(prefs.backoff_multiplier));
r.get(&prefs.sf, sizeof(prefs.sf));
r.get(&prefs.cr, sizeof(prefs.cr));
r.get(&prefs.allow_read_only, sizeof(prefs.allow_read_only));
r.get(&prefs.multi_acks, sizeof(prefs.multi_acks));
r.get(&prefs.bw, sizeof(prefs.bw));
/* 120: leds_disabled, magic-encoded. Formerly agc_reset_interval — see the
* LEDS_PREF_* comment in NodePrefs.h for why this is not a bare 0/1.
* leds_byte stays 0 (→ LEDs on) if the file is short. */
uint8_t leds_byte = 0;
r.get(&leds_byte, sizeof(leds_byte));
r.get(&prefs.path_hash_mode, sizeof(prefs.path_hash_mode));
r.get(&prefs.loop_detect, sizeof(prefs.loop_detect));
r.skip(1);
r.get(&prefs.flood_max, sizeof(prefs.flood_max));
r.get(&prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval));
r.get(&prefs.interference_threshold, sizeof(prefs.interference_threshold));
r.skip(25); // bridge settings
r.get(&prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled));
r.skip(3);
r.get(&prefs.gps_enabled, sizeof(prefs.gps_enabled));
r.get(&prefs.gps_interval, sizeof(prefs.gps_interval));
r.get(&prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy));
r.get(&prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp));
r.get(&prefs.adc_multiplier, sizeof(prefs.adc_multiplier));
r.get(prefs.owner_info, sizeof(prefs.owner_info));
/* ZephCore extensions — absent in old files; a field past the end keeps
* the caller's initNodePrefs() default. */
r.get(&prefs.rx_boost, sizeof(prefs.rx_boost));
r.get(&prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
/* RESERVED — formerly apc_enabled / apc_margin (APC, removed in 1.16.6).
* Still consumed so the fields after them stay at their stored offsets. */
r.get(&prefs._reserved_apc_enabled, sizeof(prefs._reserved_apc_enabled));
r.get(&prefs._reserved_apc_margin, sizeof(prefs._reserved_apc_margin));
r.get(&prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped));
r.get(&prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
r.get(&prefs.meshtimesync, sizeof(prefs.meshtimesync));
r.get(&prefs.cad_auto, sizeof(prefs.cad_auto));
r.get(&prefs.cad_offset, sizeof(prefs.cad_offset));
r.get(&prefs.probe_interval, sizeof(prefs.probe_interval));
r.get(&prefs.cad_busycap, sizeof(prefs.cad_busycap));
r.get(prefs.extra_sf, sizeof(prefs.extra_sf));
r.get(&prefs.fem_rxgain, sizeof(prefs.fem_rxgain));
r.get(&prefs.display_rotate, sizeof(prefs.display_rotate));
r.get(&prefs.input_rotate, sizeof(prefs.input_rotate));
/* cad_base 0 = "no base recorded": setCadParams() then leaves the stored
* offset alone, since a node upgrading across a table change cannot know
* which base its offset came from. */
r.get(&prefs.cad_base, sizeof(prefs.cad_base));
r.get(&prefs.tz_offset, sizeof(prefs.tz_offset));
r.get(&prefs.leds_radio_mode, sizeof(prefs.leds_radio_mode));
r.get(&prefs.leds_hb_mode, sizeof(prefs.leds_hb_mode));
/* led_brightness (ZephCore extension, since 1.17.4). Absent in files
* written before this field existed; the no-op EOF read leaves the
* initNodePrefs() default ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT (10%),
* same value a fresh node already showed before this was persisted. */
r.get(&prefs.led_brightness, sizeof(prefs.led_brightness));
/* Only the explicit "off" magic disables LEDs; a legacy AGC interval or an
* unwritten byte both mean "on". */
prefs.leds_disabled = (leds_byte == LEDS_PREF_OFF) ? 1 : 0;
if (prefs.freq < ZC_RADIO_FREQ_MIN_MHZ || prefs.freq > ZC_RADIO_FREQ_MAX_MHZ ||
prefs.sf < 5 || prefs.sf > 12 ||
prefs.bw < ZC_RADIO_BW_MIN_KHZ || prefs.bw > ZC_RADIO_BW_MAX_KHZ) {
prefs.freq = mesh::LoRaConfig::FREQ_MHZ;
prefs.bw = mesh::LoRaConfig::BANDWIDTH_KHZ;
prefs.sf = mesh::LoRaConfig::SPREADING_FACTOR;
prefs.cr = mesh::LoRaConfig::CODING_RATE;
prefs.tx_power_dbm = mesh::LoRaConfig::TX_POWER_DBM;
}
/* Bounds, NaNs, and the char fields, which the file stores without
* terminators. */
sanitizeNodePrefs(&prefs);
}