mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-25 13:24:36 +00:00
MeshCore names run to 31 characters (ContactInfo::name[32]), but UIMessage::sender held 24, so every longer name was cut on the way in. d304d3f made the cut visible instead of letting it push the author into the message body; this makes it stop happening. MAX_SENDER_NAME goes to 31 — exactly the wire width, so nothing is lost. Truncation could never fix three of these. "Ack" and "Mention" insert @[<sender>] into the composer, and textMentionsMe needs the whole name followed by ']', so a mention of a long-named node never reached them. Block-by-name compares the stored name against the sender on the RX path; for ROOM posts those two came from different widths already — the check sees the untruncated 31-char author while the stored entry came from the 24-char field — so a room author with a long name has simply been unblockable. Info and the chat-list preview showed a cut name. The obvious change — widening UiHistoryMsg::sender — would have been quietly destructive, so the on-disk width is frozen at 25 instead and the overflow is APPENDED to UiSegMsg, after seq. sender sits between thread and text, so widening it in place shifts text for every record already on disk, and nothing would have caught that: the segment store is what actually holds messages now, and uiSegOpenValidated checks only magic/version/rec_size — it never consults k_ui_history_version or k_ui_history_min_version, which guard the legacy files alone. An old segment would have passed validation, prefix-read into the new offsets, lost the first 8 characters of every message body and read seq as 0 (so loadMsgsFromSegments renumbers the lot), and the next compaction would have written that back permanently. Appending at the tail is the evolution path UiSegHeader already documents, and it is correct in both directions: an old 233-byte record zero-fills the new field and yields its original short name, and older firmware reading a new 240-byte record copies the first 233 bytes and ignores the tail. That second half is why k_ui_seg_version STAYS 1 — bumping it would make older firmware reject the file outright and quarantine a history it could have read. So there is no migration, no version bump, and no wiped chat history. static_asserts now pin the offsets an already-written record depends on. TOUCH_IGNORED_NAME_LEN has to move in lockstep (28 -> 32) or blocking breaks: a slot narrower than a sender stores a cut name the full-width check can never match, and the block sits in Settings doing nothing. Its NVS blob has no header — the entry count is its length divided by the slot width — so re-slotting in place would mangle every entry after the first and the next write would make that stick. The key is renamed ign_nm -> ign_nm2 and the old blob folded in once at first read, which is unambiguous in a way that sniffing the blob length is not (28 and 32 share multiples at 224 and 448, both inside the 16-entry cap). The invariant the header only stated in prose is a static_assert now. Split/rejoin is a host-tested header next to ChannelSenderSplit, with the field-width bounds spelled out: a corrupt record can leave the name field unterminated, and the message body is what follows it on disk. Signed-off-by: Ruben Laban <ruben@tun0.nl>
90 lines
4.0 KiB
C++
90 lines
4.0 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
|
|
#include "ui-touch/SenderExtField.h"
|
|
|
|
// The on-disk shape: base is the frozen v6 field (24 chars + NUL), ext is the appended
|
|
// tail, and together they hold MAX_SENDER_NAME == 31 chars.
|
|
static const size_t kBaseCap = 25;
|
|
static const size_t kExtCap = 7;
|
|
static const size_t kOutCap = 32;
|
|
|
|
int main() {
|
|
char base[kBaseCap], ext[kExtCap], out[kOutCap];
|
|
|
|
// Short name: fits the base field, ext stays empty.
|
|
SenderExtField::store("nick", base, sizeof base, ext, sizeof ext);
|
|
assert(strcmp(base, "nick") == 0);
|
|
for (size_t i = 0; i < kExtCap; ++i) assert(ext[i] == '\0');
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(strcmp(out, "nick") == 0);
|
|
|
|
// Exactly 24: fills the base field with no spill. The tail must stay empty, or the
|
|
// "base is full" test below would resurrect garbage.
|
|
const char* exactly24 = "123456789012345678901234";
|
|
assert(strlen(exactly24) == 24);
|
|
SenderExtField::store(exactly24, base, sizeof base, ext, sizeof ext);
|
|
assert(strcmp(base, exactly24) == 0);
|
|
for (size_t i = 0; i < kExtCap; ++i) assert(ext[i] == '\0');
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(strcmp(out, exactly24) == 0);
|
|
|
|
// A full 31-char name — the case the whole split exists for. Round-trips intact.
|
|
const char* full = "Ouderkerk Repeater Node Twelve";
|
|
assert(strlen(full) == 30);
|
|
SenderExtField::store(full, base, sizeof base, ext, sizeof ext);
|
|
assert(strncmp(base, full, 24) == 0 && base[24] == '\0');
|
|
assert(memcmp(ext, full + 24, 6) == 0);
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(strcmp(out, full) == 0);
|
|
|
|
// The longest possible name fills ext completely, leaving it unterminated. load() is
|
|
// bounded by the field size, so it must not read past.
|
|
const char* longest = "1234567890123456789012345678901";
|
|
assert(strlen(longest) == 31);
|
|
SenderExtField::store(longest, base, sizeof base, ext, sizeof ext);
|
|
assert(memcmp(ext, longest + 24, kExtCap) == 0); // no NUL anywhere in ext
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(strcmp(out, longest) == 0);
|
|
|
|
// An OLD record: base written by a pre-widening build, ext zero-filled by the loader's
|
|
// prefix read. Must yield exactly the stored short name.
|
|
memset(ext, 0, sizeof ext);
|
|
memset(base, 0, sizeof base);
|
|
strcpy(base, "olde name");
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(strcmp(out, "olde name") == 0);
|
|
|
|
// Garbage in ext behind a NON-full base must be ignored — that is the corrupt-record
|
|
// case, and appending it would splice junk onto a valid short name.
|
|
memset(ext, 'X', sizeof ext);
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(strcmp(out, "olde name") == 0);
|
|
|
|
// An unterminated base field (corrupt record) must not read into the message body that
|
|
// follows it on disk: the copy stops at the field width.
|
|
char raw_base[kBaseCap];
|
|
memset(raw_base, 'A', sizeof raw_base); // no NUL at all
|
|
memset(ext, 0, sizeof ext);
|
|
SenderExtField::load(raw_base, sizeof raw_base, ext, sizeof ext, out, sizeof out);
|
|
assert(strlen(out) == 24);
|
|
|
|
// A multi-byte character straddling the boundary: split anywhere, rejoin exact. The
|
|
// halves are never used apart, so the byte-level split is deliberately not aligned.
|
|
const char* utf8 = "Ouderkerkkkkkkkkkkkkkkk\xE2\x98\x80"; // 23 ASCII + 3-byte U+2600
|
|
assert(strlen(utf8) == 26);
|
|
SenderExtField::store(utf8, base, sizeof base, ext, sizeof ext);
|
|
assert((unsigned char)base[23] == 0xE2); // sequence is cut mid-character on disk
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(strcmp(out, utf8) == 0); // and comes back whole
|
|
|
|
// Degenerate inputs.
|
|
SenderExtField::store(nullptr, base, sizeof base, ext, sizeof ext);
|
|
assert(base[0] == '\0');
|
|
SenderExtField::load(base, sizeof base, ext, sizeof ext, out, sizeof out);
|
|
assert(out[0] == '\0');
|
|
return 0;
|
|
}
|