// SPDX-License-Identifier: GPL-3.0-or-later #include #include #include "helpers/esp32/TouchPrefsSchema.h" using TouchPrefsSchema::Config; static Config safeDefaults() { Config c = {}; c.magic = TouchPrefsSchema::MAGIC; c.ver = TouchPrefsSchema::CURRENT_VERSION; c.bright = 100; c.rx_queue = 1; c.web_mirror = 0; c.remote_mode = 0; c.remote_landscape = 1; c.web_terminal = 0; c.map_tile_debug = 0; c.hist_sync_after = 2; c.hist_per_chat = 250; c.p4_antenna = 0; c.retry_echo = 1; c.console_monitor = 1; c.boot_wifi_time = 0; c.boot_wifi_open = 0; c.theme_mode = 0; c.gps_fuzz_m = 0; c.attaky_notify_enabled = 0; c.attaky_notify_room_color = 0; c.attaky_notify_dm_color = 1; c.home_key_keeps_drawer = 0; return c; } int main() { constexpr size_t v43_size = offsetof(Config, retry_echo); constexpr size_t suffix_offset = offsetof(Config, web_mirror); constexpr size_t v43_suffix_size = v43_size - suffix_offset; static_assert(offsetof(Config, retry_echo) == offsetof(Config, p4_antenna) + sizeof(Config::p4_antenna), "v45 retry field moved"); // The tail is append-only, so a blob written by an older firmware is always a // strict PREFIX of the current layout. Asserting an exact size delta pins the // test to one release and goes stale the moment the next field lands (it did: // this used to demand "v45 appends exactly one byte"). Assert the invariant // that actually matters instead — every historical blob is shorter, and the // fields it never stored keep their defaults. static_assert(v43_size < sizeof(Config), "v43 blobs must be a strict prefix of the current layout"); // A v43 blob has the current layout truncated before the newly appended // retry byte. Every established field must survive byte-for-byte, and every // field appended since must fail safe to its default. Config v43 = safeDefaults(); v43.ver = 43; v43.bright = 67; v43.rx_queue = 0; v43.web_mirror = 1; v43.remote_mode = 1; v43.remote_landscape = 0; v43.web_terminal = 1; v43.map_tile_debug = 1; v43.hist_sync_after = 7; v43.hist_per_chat = 0x1234; v43.p4_antenna = 0x56; v43.retry_echo = 0; // outside the stored v43 extent v43.boot_wifi_time = 1; // ditto — must NOT reach the migrated config v43.boot_wifi_open = 1; Config migrated = safeDefaults(); uint8_t stored_version = 0; assert(TouchPrefsSchema::overlayStored(migrated, &v43, v43_size, &stored_version)); assert(stored_version == 43); assert(migrated.bright == 67); assert(migrated.rx_queue == 0); assert(migrated.web_mirror == 1); assert(migrated.remote_mode == 1); assert(migrated.remote_landscape == 0); assert(migrated.web_terminal == 1); assert(migrated.map_tile_debug == 1); assert(migrated.hist_sync_after == 7); assert(migrated.hist_per_chat == 0x1234); assert(migrated.p4_antenna == 0x56); assert(migrated.retry_echo == 1); assert(migrated.boot_wifi_time == 0); assert(migrated.boot_wifi_open == 0); assert(migrated.loud_alerts == 0); assert(migrated.theme_mode == 0); assert(migrated.gps_fuzz_m == 0); assert(migrated.attaky_notify_enabled == 0); assert(migrated.attaky_notify_room_color == 0); assert(migrated.attaky_notify_dm_color == 1); // Reproduce beta 57 exactly: retry was inserted before the old suffix, then // the v43 bytes were copied and the full shifted v44 structure was written. uint8_t broken_v44[sizeof(Config)] = {}; memcpy(broken_v44, &v43, suffix_offset); broken_v44[offsetof(Config, ver)] = TouchPrefsSchema::BROKEN_MID_INSERT_VERSION; broken_v44[suffix_offset] = 1; // inserted retry_echo, overwriting old web_mirror memcpy(broken_v44 + suffix_offset + 1, reinterpret_cast(&v43) + suffix_offset, v43_suffix_size); const Config defaults = safeDefaults(); migrated = defaults; stored_version = 0; assert(TouchPrefsSchema::overlayStored(migrated, broken_v44, sizeof(broken_v44), &stored_version)); assert(stored_version == 44); assert(migrated.bright == 67); // unambiguous prefix is preserved assert(migrated.rx_queue == 0); assert(memcmp(reinterpret_cast(&migrated) + suffix_offset, reinterpret_cast(&defaults) + suffix_offset, sizeof(Config) - suffix_offset) == 0); // ambiguous suffix fails safe // #383 appended boot_wifi_time + boot_wifi_open at the tail. A v53 blob is the // current layout minus exactly those two bytes: everything before them must // survive, and both opt-ins must come back OFF rather than inheriting whatever // byte followed the blob — an inherited 1 would spend boot time on a Wi-Fi // session the user never asked for. constexpr size_t v53_size = offsetof(Config, boot_wifi_time); static_assert(v53_size + sizeof(Config::boot_wifi_time) + sizeof(Config::boot_wifi_open) + sizeof(Config::loud_alerts) + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m) + sizeof(Config::attaky_notify_enabled) + sizeof(Config::attaky_notify_room_color) + sizeof(Config::attaky_notify_dm_color) + sizeof(Config::telem_loc_exact) + sizeof(Config::home_key_keeps_drawer) + sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout) + sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type) + sizeof(Config::ble_kbd_name) + sizeof(Config::ble_kbd_back) == sizeof(Config), "v53 is the current layout minus every byte appended since"); Config v53 = safeDefaults(); v53.ver = 53; v53.bright = 42; v53.retry_echo = 0; v53.console_mode = 1; v53.console_monitor = 0; v53.kb_force_legacy = 1; v53.boot_wifi_time = 1; // outside the stored v53 extent v53.boot_wifi_open = 1; migrated = safeDefaults(); migrated.boot_wifi_time = 0; migrated.boot_wifi_open = 0; stored_version = 0; assert(TouchPrefsSchema::overlayStored(migrated, &v53, v53_size, &stored_version)); assert(stored_version == 53); assert(migrated.bright == 42); assert(migrated.retry_echo == 0); assert(migrated.console_mode == 1); assert(migrated.console_monitor == 0); assert(migrated.kb_force_legacy == 1); assert(migrated.boot_wifi_time == 0); assert(migrated.boot_wifi_open == 0); // Once rewritten at the current version the whole structure is authoritative, // including an explicit retry opt-out, a deliberately enabled Remote Mode, and // both #383 opt-ins turned on. Config current = safeDefaults(); current.remote_mode = 1; current.remote_landscape = 0; current.retry_echo = 0; current.boot_wifi_time = 1; current.boot_wifi_open = 1; current.loud_alerts = 1; current.theme_mode = 1; current.gps_fuzz_m = 150; current.attaky_notify_enabled = 1; current.attaky_notify_room_color = 6; current.attaky_notify_dm_color = 4; current.home_key_keeps_drawer = 1; current.ble_kbd_mode = 1; current.ble_kbd_layout = 4; const uint8_t kbd_addr[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; memcpy(current.ble_kbd_addr, kbd_addr, sizeof kbd_addr); current.ble_kbd_addr_type = 1; strncpy(current.ble_kbd_name, "Test keyboard", sizeof current.ble_kbd_name - 1); current.ble_kbd_back = 0x64; migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, ¤t, sizeof(current), &stored_version)); assert(stored_version == TouchPrefsSchema::CURRENT_VERSION); assert(memcmp(&migrated, ¤t, sizeof(current)) == 0); // Once rewritten as v45, every field through retry_echo is authoritative; // fields appended by later versions retain their safe defaults. constexpr size_t v45_size = offsetof(Config, app_hide); Config v45 = safeDefaults(); v45.ver = 45; v45.remote_mode = 1; v45.remote_landscape = 0; v45.retry_echo = 0; migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, &v45, v45_size, &stored_version)); assert(stored_version == 45); assert(migrated.remote_mode == 1); assert(migrated.remote_landscape == 0); assert(migrated.retry_echo == 0); assert(migrated.app_hide == safeDefaults().app_hide); assert(migrated.theme_mode == 0); // v56 appended only theme_mode. It was written as v54 on the contributing // branch, but v54 and v55 were taken by boot_wifi_* and loud_alerts before it // merged, so the field sits behind those and a historical v55 blob must leave // it at the Night default. constexpr size_t v55_size = offsetof(Config, theme_mode); static_assert(v55_size + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m) + sizeof(Config::attaky_notify_enabled) + sizeof(Config::attaky_notify_room_color) + sizeof(Config::attaky_notify_dm_color) + sizeof(Config::telem_loc_exact) + sizeof(Config::home_key_keeps_drawer) + sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout) + sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type) + sizeof(Config::ble_kbd_name) + sizeof(Config::ble_kbd_back) == sizeof(Config), "v55 is the current layout minus every byte appended since"); Config v55 = safeDefaults(); v55.ver = 55; v55.kb_force_legacy = 1; v55.loud_alerts = 1; v55.theme_mode = 1; // outside the stored v55 extent migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, &v55, v55_size, &stored_version)); assert(stored_version == 55); assert(migrated.kb_force_legacy == 1); assert(migrated.loud_alerts == 1); assert(migrated.theme_mode == 0); assert(migrated.gps_fuzz_m == 0); assert(migrated.attaky_notify_enabled == 0); assert(migrated.attaky_notify_room_color == 0); assert(migrated.attaky_notify_dm_color == 1); constexpr size_t v57_size = offsetof(Config, attaky_notify_enabled); static_assert(v57_size + sizeof(Config::attaky_notify_enabled) + sizeof(Config::attaky_notify_room_color) + sizeof(Config::attaky_notify_dm_color) + sizeof(Config::telem_loc_exact) + sizeof(Config::home_key_keeps_drawer) + sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout) + sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type) + sizeof(Config::ble_kbd_name) + sizeof(Config::ble_kbd_back) == sizeof(Config), "v57 is the current layout minus the Attaky notification fields"); Config v57 = safeDefaults(); v57.ver = 57; v57.gps_fuzz_m = 250; v57.attaky_notify_enabled = 1; // outside the stored v57 extent v57.attaky_notify_room_color = 6; v57.attaky_notify_dm_color = 5; migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, &v57, v57_size, &stored_version)); assert(stored_version == 57); assert(migrated.gps_fuzz_m == 250); assert(migrated.attaky_notify_enabled == 0); assert(migrated.attaky_notify_room_color == 0); assert(migrated.attaky_notify_dm_color == 1); // #491 appends the M9 Home-key drawer behavior at v60. A v59 blob keeps // every prior setting, while the new option defaults OFF to preserve the // established Commander/drawer toggle until the user opts in. constexpr size_t v59_size = offsetof(Config, home_key_keeps_drawer); static_assert(v59_size + sizeof(Config::home_key_keeps_drawer) + sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout) + sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type) + sizeof(Config::ble_kbd_name) + sizeof(Config::ble_kbd_back) == sizeof(Config), "v59 is the current layout minus the Home-key drawer option"); Config v59 = safeDefaults(); v59.ver = 59; v59.telem_loc_exact = 1; v59.home_key_keeps_drawer = 1; // outside the stored v59 extent migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, &v59, v59_size, &stored_version)); assert(stored_version == 59); assert(migrated.telem_loc_exact == 1); assert(migrated.home_key_keeps_drawer == 0); // v61 appends the Bluetooth keyboard settings. A v60 blob keeps the Home-key // option, and Bluetooth stays with the phone app with no keyboard paired, // whatever bytes followed the old blob. constexpr size_t v60_size = offsetof(Config, ble_kbd_mode); static_assert(v60_size + sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout) + sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type) + sizeof(Config::ble_kbd_name) + sizeof(Config::ble_kbd_back) == sizeof(Config), "v60 is the current layout minus the Bluetooth keyboard settings"); Config v60 = safeDefaults(); v60.ver = 60; v60.home_key_keeps_drawer = 1; v60.ble_kbd_mode = 1; // outside the stored v60 extent v60.ble_kbd_layout = 3; v60.ble_kbd_addr[0] = 0xAA; migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, &v60, v60_size, &stored_version)); assert(stored_version == 60); assert(migrated.home_key_keeps_drawer == 1); assert(migrated.ble_kbd_mode == 0); assert(migrated.ble_kbd_layout == 0); assert(migrated.ble_kbd_addr[0] == 0); assert(migrated.ble_kbd_name[0] == '\0'); assert(migrated.ble_kbd_back == 0); // v62 appends the extra Back key. A v61 blob keeps the paired keyboard and // its settings; only Esc goes back until the user picks another key. constexpr size_t v61_size = offsetof(Config, ble_kbd_back); static_assert(v61_size + sizeof(Config::ble_kbd_back) == sizeof(Config), "v61 is the current layout minus the Back key"); Config v61 = safeDefaults(); v61.ver = 61; v61.ble_kbd_mode = 1; v61.ble_kbd_layout = 4; v61.ble_kbd_addr[5] = 0x4D; v61.ble_kbd_addr_type = 1; strncpy(v61.ble_kbd_name, "Sologic - Keyboard", sizeof v61.ble_kbd_name - 1); v61.ble_kbd_back = 0x64; // outside the stored v61 extent migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, &v61, v61_size, &stored_version)); assert(stored_version == 61); assert(migrated.ble_kbd_mode == 1); assert(migrated.ble_kbd_layout == 4); assert(migrated.ble_kbd_addr[5] == 0x4D); assert(migrated.ble_kbd_addr_type == 1); assert(strcmp(migrated.ble_kbd_name, "Sologic - Keyboard") == 0); assert(migrated.ble_kbd_back == 0); Config invalid = safeDefaults(); uint8_t garbage[sizeof(Config)] = {}; assert(!TouchPrefsSchema::overlayStored(invalid, garbage, sizeof(garbage))); assert(memcmp(&invalid, &defaults, sizeof(defaults)) == 0); return 0; }