diff --git a/examples/companion_radio/ui-core/ContactControl.h b/examples/companion_radio/ui-core/ContactControl.h index 6a3b06d4..b379eda1 100644 --- a/examples/companion_radio/ui-core/ContactControl.h +++ b/examples/companion_radio/ui-core/ContactControl.h @@ -90,7 +90,9 @@ inline uint8_t hopCount(uint8_t path_len_packed) { inline void hopName(uint8_t path_len_packed, const uint8_t* path, int i, char* out, size_t n) { uint8_t hash_size = (path_len_packed >> 6) + 1; const uint8_t* hash = &path[i * hash_size]; - for (int idx = 0; ; idx++) { + // From MAX_ANON_CONTACTS: the reserved slots before it are empty or anon + // requesters, and an empty one matched a 00 hash with a blank name. + for (int idx = MAX_ANON_CONTACTS; ; idx++) { ContactInfo c; if (!the_mesh.getContactByIdx(idx, c)) break; if (c.id.isHashMatch(hash, hash_size)) { snprintf(out, n, "%s", c.name); return; } diff --git a/examples/companion_radio/ui-core/NearbyModel.h b/examples/companion_radio/ui-core/NearbyModel.h index 1c42196e..732d752c 100644 --- a/examples/companion_radio/ui-core/NearbyModel.h +++ b/examples/companion_radio/ui-core/NearbyModel.h @@ -173,10 +173,12 @@ public: void refreshModel() { if (_source == SRC_SCAN) refreshScan(); else refreshStored(); } - // Index of the row for raw contact index `contact_idx`, or of the non-contact - // live row named `name` -- how a screen keeps its selection across a rebuild. - int findContact(int contact_idx) const { - for (int i = 0; i < _count; i++) if (_entries[i].contact_idx == contact_idx) return i; + // Index of the contact row with this public key, or of the non-contact live + // row named `name` -- how a screen keeps its selection across a rebuild. By + // key, not by contact index: deleting a contact shifts the table down. + int findContact(const uint8_t* pub_key) const { + for (int i = 0; i < _count; i++) + if (_entries[i].contact_idx >= 0 && memcmp(_entries[i].pub_key, pub_key, PUB_KEY_SIZE) == 0) return i; return -1; } int findLiveByName(const char* name) const { diff --git a/examples/companion_radio/ui-lvgl/HistoryStore.h b/examples/companion_radio/ui-lvgl/HistoryStore.h index 1ac00be7..e053c1a3 100644 --- a/examples/companion_radio/ui-lvgl/HistoryStore.h +++ b/examples/companion_radio/ui-lvgl/HistoryStore.h @@ -177,7 +177,12 @@ public: } void clearAll() { if (!ready()) return; - forEachFile([](const char* path, char) { remove(path); }); + forEachFile([](const char* path, char) { + remove(path); + char bad[72]; // a set-aside copy (see append()) goes with it: append() always recreates the .bin + snprintf(bad, sizeof(bad), "%s.bad", path); + remove(bad); + }); } private: @@ -241,7 +246,15 @@ private: f = fopen(path, "r+b"); } if (!f || !readHdr(f, h)) { // new (or unreadable: start it again) - if (f) fclose(f); + if (f) { + // Unreadable (a damaged header, a card edited elsewhere): set it aside + // as .bad -- one copy, the newest -- instead of writing over the history. + fclose(f); + char bad[72]; + snprintf(bad, sizeof(bad), "%s.bad", path); + remove(bad); + rename(path, bad); + } f = fopen(path, "w+b"); if (!f) return; h = { MAGIC, VERSION, (uint16_t)sizeof(E), _keep, 0, 0, 1 }; diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index 133d0b3c..eb57906b 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -993,7 +993,10 @@ void UITask::navToolsPopup() { _nav_share_lbl = noteLabel(list, "", THEME_FONT_SMALL, theme::TEXT); // Target: channels (as named -- "#" marks a hashtag channel), then // favourite contacts (starred, so the two stay apart). - char opts[MAX_GROUP_CHANNELS * 24 + 16 * 40]; + // Worst case: "\n" + a 31-byte name per channel, "\n" + star (3) + space + + // 31 bytes per favourite. Static: ~1.9 KB is a lot for the loop's stack, and + // the dropdown copies it. + static char opts[MAX_GROUP_CHANNELS * 32 + 16 * 36 + 1]; int o = 0, sel = 0; navmap::s_share_n = 0; for (int i = 0; i < MAX_GROUP_CHANNELS && navmap::s_share_n < navmap::SHARE_TARGETS; i++) { diff --git a/examples/companion_radio/ui-lvgl/map/VectorPacks.h b/examples/companion_radio/ui-lvgl/map/VectorPacks.h index 0223190e..02a6a932 100644 --- a/examples/companion_radio/ui-lvgl/map/VectorPacks.h +++ b/examples/companion_radio/ui-lvgl/map/VectorPacks.h @@ -59,6 +59,7 @@ public: Pack& p = _p[i]; uint32_t off, len; if (!lookup(p, key, off, len)) continue; + if ((uint64_t)p.data_off + off + len > p.size) return false; // an entry pointing past the file FILE* f = handle(i); if (!f || fseek(f, p.data_off + off, SEEK_SET) != 0) return false; s.f = f; s.len = len; s.owned = false; @@ -128,8 +129,10 @@ private: fseek(f, 0, SEEK_END); p.size = (uint32_t)ftell(f); p.index = nullptr; - uint32_t isz = p.count * 16; - ok = p.index_off + isz <= p.size && p.data_off <= p.size; + // In 64 bits / by division: a damaged count would otherwise wrap count * 16 + // to a small size, pass, and send the index search far past its buffer. + ok = p.count <= p.size / 16 && (uint64_t)p.index_off + (uint64_t)p.count * 16 <= p.size && p.data_off <= p.size; + uint32_t isz = ok ? p.count * 16 : 0; if (ok && isz <= INDEX_IN_RAM_MAX) { p.index = psramBuf(isz ? isz : 1); if (p.index && (fseek(f, p.index_off, SEEK_SET) != 0 || !readFast(f, p.index, isz))) { free(p.index); p.index = nullptr; } diff --git a/examples/companion_radio/ui-new/HomeScreen.h b/examples/companion_radio/ui-new/HomeScreen.h index 8d60101f..50b287f5 100644 --- a/examples/companion_radio/ui-new/HomeScreen.h +++ b/examples/companion_radio/ui-new/HomeScreen.h @@ -1491,7 +1491,7 @@ public: } } } else if (prefix) { - for (int idx = 0; ; idx++) { + for (int idx = MAX_ANON_CONTACTS; ; idx++) { ContactInfo c; if (!the_mesh.getContactByIdx(idx, c)) break; if (memcmp(c.id.pub_key, prefix, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { @@ -1673,7 +1673,7 @@ public: _task->openChannelHistory(pfx[0]); return true; } - for (int idx = 0; ; idx++) { + for (int idx = MAX_ANON_CONTACTS; ; idx++) { ContactInfo c2; if (!the_mesh.getContactByIdx(idx, c2)) break; if (memcmp(c2.id.pub_key, pfx, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { diff --git a/examples/companion_radio/ui-new/LiveShareScreen.h b/examples/companion_radio/ui-new/LiveShareScreen.h index 5cb78900..93d2d6a8 100644 --- a/examples/companion_radio/ui-new/LiveShareScreen.h +++ b/examples/companion_radio/ui-new/LiveShareScreen.h @@ -157,14 +157,17 @@ public: } void cycleTarget(int dir) { - struct T { uint8_t type, ch; uint8_t prefix[NodePrefs::FAVOURITE_PREFIX_LEN]; } list[24]; + // Every channel and every dial favourite: a cap under MAX_GROUP_CHANNELS + // (40) left the favourites, listed last, out of reach. + static const int LIST_MAX = MAX_GROUP_CHANNELS + NodePrefs::FAVOURITES_COUNT; + struct T { uint8_t type, ch; uint8_t prefix[NodePrefs::FAVOURITE_PREFIX_LEN]; } list[LIST_MAX]; int n = 0; ChannelDetails ch; - for (int i = 0; i < 64 && n < 24; i++) { + for (int i = 0; i < MAX_GROUP_CHANNELS && n < LIST_MAX; i++) { if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') continue; list[n].type = 0; list[n].ch = (uint8_t)i; n++; } - for (int i = 0; i < NodePrefs::FAVOURITES_COUNT && n < 24; i++) { + for (int i = 0; i < NodePrefs::FAVOURITES_COUNT && n < LIST_MAX; i++) { const uint8_t* pre = _prefs->favourite_contacts[i]; bool empty = true; for (int b = 0; b < NodePrefs::FAVOURITE_PREFIX_LEN; b++) if (pre[b]) { empty = false; break; } diff --git a/examples/companion_radio/ui-new/LocatorScreen.h b/examples/companion_radio/ui-new/LocatorScreen.h index 4d9dfa3f..cac0e639 100644 --- a/examples/companion_radio/ui-new/LocatorScreen.h +++ b/examples/companion_radio/ui-new/LocatorScreen.h @@ -195,13 +195,13 @@ public: none.kind = 2; none.lat = 0; none.lon = 0; none.ts = 0; none.live = false; none.fav = false; memset(none.key, 0, 6); snprintf(none.name, sizeof(none.name), "(none)"); - for (int idx = 0; _target_n < TARGET_MAX; idx++) { + for (int idx = MAX_ANON_CONTACTS; _target_n < TARGET_MAX; idx++) { ContactInfo c; if (!the_mesh.getContactByIdx(idx, c)) break; if (!contactctl::favourite(c)) continue; addPersonTarget(c.id.pub_key, c.name, /*require_position=*/false, true); } - for (int idx = 0; _target_n < TARGET_MAX; idx++) { + for (int idx = MAX_ANON_CONTACTS; _target_n < TARGET_MAX; idx++) { ContactInfo c; if (!the_mesh.getContactByIdx(idx, c)) break; addPersonTarget(c.id.pub_key, c.name, /*require_position=*/true, contactctl::favourite(c)); diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index ef7b8053..cf7ba863 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -383,24 +383,6 @@ class MessagesScreen : public UIScreen { } } - // Resolve one hop's path hash to a contact name, falling back to a short hex - // tag ("?AABB") when no known contact matches -- an unnamed/unknown repeater, - // or one whose contact card has since been removed. - void resolveHopName(const uint8_t* hash, uint8_t hash_size, char* out, size_t out_sz) { - for (int idx = 0; ; idx++) { - ContactInfo c; - if (!the_mesh.getContactByIdx(idx, c)) break; - if (c.id.isHashMatch(hash, hash_size)) { - snprintf(out, out_sz, "%s", c.name); - return; - } - } - char hex[9] = {0}; - uint8_t n = hash_size > 4 ? 4 : hash_size; - for (uint8_t b = 0; b < n; b++) snprintf(hex + b * 2, 3, "%02X", hash[b]); - snprintf(out, out_sz, "?%s", hex); - } - // Opens the path/relay detail popup for the entry stashed by buildFsMenu(). // Incoming: hops listed oldest-hop-first (the order they were appended as the // message travelled). Outgoing channel: each distinct repeater heard echoing @@ -423,7 +405,7 @@ class MessagesScreen : public UIScreen { uint8_t max_hops = MAX_HIST_PATH_BYTES / hash_size; // matches capturePath()/markChannelRelayed()'s write-side bound if (hop_count > max_hops) hop_count = max_hops; for (uint8_t i = 0; i < hop_count; i++) { - resolveHopName(&path[i * hash_size], hash_size, _path_detail_names[i], sizeof(_path_detail_names[i])); + contactctl::hopName(path_len_packed, path, i, _path_detail_names[i], sizeof(_path_detail_names[i])); } _ctx_menu.begin(path_is_relay ? "Relayed by" : "Path", hop_count); for (uint8_t i = 0; i < hop_count; i++) _ctx_menu.addItem(_path_detail_names[i]); @@ -724,7 +706,7 @@ class MessagesScreen : public UIScreen { DisplayDriver::translateUTF8Static(nm + 1, chanctl::bareName(ch.name), sizeof(nm) - 1); } } else { - for (int idx = 0; ; idx++) { + for (int idx = MAX_ANON_CONTACTS; ; idx++) { ContactInfo c2; if (!the_mesh.getContactByIdx(idx, c2)) break; if (memcmp(c2.id.pub_key, pfx, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 0c22df0d..289e6a2b 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -105,20 +105,22 @@ class NearbyScreen : public UIScreen, protected NearbyModel { void refresh() { refreshModel(); clampSelection(); } // Rebuild the stored list (re-merge live shares + re-sort) while keeping the - // currently highlighted node selected across the rebuild — by contact index + // currently highlighted node selected across the rebuild — by public key // for a stored node, or by name for a non-contact live sender. Returns false // if that node is no longer in the list (caller decides whether to drop the // detail/nav view). Shared by the list, detail and navigate refresh paths. bool refreshKeepingSelection() { - int saved_idx = (_sel < _count) ? _entries[_sel].contact_idx : -1; - bool saved_live = (_sel < _count) && _entries[_sel].is_live && saved_idx < 0; + bool saved_contact = (_sel < _count) && _entries[_sel].contact_idx >= 0; + bool saved_live = (_sel < _count) && _entries[_sel].is_live && !saved_contact; + uint8_t saved_key[PUB_KEY_SIZE]; + if (saved_contact) memcpy(saved_key, _entries[_sel].pub_key, PUB_KEY_SIZE); char saved_name[sizeof(_entries[0].name)]; saved_name[0] = '\0'; if (_sel < _count) { strncpy(saved_name, _entries[_sel].name, sizeof(saved_name) - 1); saved_name[sizeof(saved_name) - 1] = '\0'; } refreshStored(); - int i = saved_idx >= 0 ? findContact(saved_idx) + int i = saved_contact ? findContact(saved_key) : (saved_live && saved_name[0]) ? findLiveByName(saved_name) : -1; if (i >= 0) { _sel = i; return true; } return false;