mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-23 17:57:16 +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>
144 lines
6.6 KiB
C++
144 lines
6.6 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
|
|
#include "ui-touch/ChannelSenderSplit.h"
|
|
|
|
// UIMessage::sender is char[MAX_SENDER_NAME + 1] with MAX_SENDER_NAME == 31 — the full
|
|
// MeshCore name width (ContactInfo::name[32]), so every real name now fits and the
|
|
// truncation path is unreachable from the firmware's own callers. It is still exercised
|
|
// below against narrower destinations: the helper is documented to truncate to whatever
|
|
// it is handed, and the beta that shipped a 24-char field relies on exactly that.
|
|
static const size_t kSenderCap = 32;
|
|
|
|
// The frozen on-disk width (k_ui_disk_sender_len) — and what the previous beta's
|
|
// UIMessage::sender was. The truncation + UTF-8 back-off cases run against this.
|
|
static const size_t kNarrowCap = 25;
|
|
|
|
int main() {
|
|
char sender[kSenderCap];
|
|
|
|
// Ordinary channel post: author out, body in.
|
|
const char* body = ChannelSenderSplit::split("nick: hello there", sender, sizeof sender);
|
|
assert(strcmp(sender, "nick") == 0);
|
|
assert(strcmp(body, "hello there") == 0);
|
|
|
|
// The regression this all started from: a 27-char name. It used to fail the split
|
|
// outright, so the bubble header showed the channel and the body kept reading
|
|
// "nick: message". At the full width it is stored whole, with no marker.
|
|
const char* long_name = "Ouderkerk Repeater Node Two: on air";
|
|
body = ChannelSenderSplit::split(long_name, sender, sizeof sender);
|
|
assert(strcmp(sender, "Ouderkerk Repeater Node Two") == 0);
|
|
assert(strlen(sender) == 27);
|
|
assert(strcmp(body, "on air") == 0);
|
|
|
|
// The longest name the wire can carry: 31 chars, stored exactly, no marker.
|
|
const char* longest = "1234567890123456789012345678901: body";
|
|
body = ChannelSenderSplit::split(longest, sender, sizeof sender);
|
|
assert(strlen(sender) == 31);
|
|
assert(strcmp(sender, "1234567890123456789012345678901") == 0);
|
|
assert(strcmp(body, "body") == 0);
|
|
|
|
// One char PAST the wire width is not a name at all — no node can be called this — so
|
|
// the ": " belongs to the message. Leave the text whole rather than inventing an author
|
|
// and hiding the start of the body.
|
|
const char* not_a_name = "12345678901234567890123456789012: body";
|
|
assert(strlen(not_a_name) == 38);
|
|
body = ChannelSenderSplit::split(not_a_name, sender, sizeof sender);
|
|
assert(sender[0] == '\0');
|
|
assert(body == not_a_name);
|
|
|
|
// The realistic shape of that: an unprefixed post whose body just happens to contain
|
|
// ": ". It must survive intact, whole, with the caller's own sender label.
|
|
const char* prose = "the repeater at Ouderkerk is back up: full quieting again";
|
|
body = ChannelSenderSplit::split(prose, sender, sizeof sender);
|
|
assert(sender[0] == '\0');
|
|
assert(body == prose);
|
|
|
|
// ---- Narrower destination: truncate the label, never reject the split -------------
|
|
char narrow[kNarrowCap];
|
|
|
|
// The 27-char name again. Too long for this field, so it is cut and marked with "..."
|
|
// — a shortened name must not read as a different node.
|
|
body = ChannelSenderSplit::split(long_name, narrow, sizeof narrow);
|
|
assert(strlen(narrow) == 24);
|
|
assert(strcmp(narrow, "Ouderkerk Repeater No...") == 0);
|
|
assert(strcmp(body, "on air") == 0);
|
|
|
|
// The widest name the wire can carry still splits here — cut, but the author is out
|
|
// of the body, which is the whole point.
|
|
body = ChannelSenderSplit::split(longest, narrow, sizeof narrow);
|
|
assert(strcmp(narrow, "123456789012345678901...") == 0);
|
|
assert(strcmp(body, "body") == 0);
|
|
|
|
// One char over the cap still gets the marker (name budget 21, not 24).
|
|
const char* one_over = "1234567890123456789012345: body";
|
|
body = ChannelSenderSplit::split(one_over, narrow, sizeof narrow);
|
|
assert(strcmp(narrow, "123456789012345678901...") == 0);
|
|
assert(strcmp(body, "body") == 0);
|
|
|
|
// Exactly at the cap: 24 chars, no truncation, no marker.
|
|
const char* exact = "123456789012345678901234: body";
|
|
body = ChannelSenderSplit::split(exact, narrow, sizeof narrow);
|
|
assert(strcmp(narrow, "123456789012345678901234") == 0);
|
|
assert(strcmp(body, "body") == 0);
|
|
|
|
// Truncation lands on a character boundary, never inside a multi-byte sequence.
|
|
// "Ouderkerkk" (10) + U+2600 (3 bytes) x 5 = 25 bytes; the 21-byte name budget
|
|
// (24 minus the marker) falls inside the fourth sun, so it backs off to 19.
|
|
const char* utf8_name = "Ouderkerkk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80: hi";
|
|
body = ChannelSenderSplit::split(utf8_name, narrow, sizeof narrow);
|
|
assert(strcmp(narrow, "Ouderkerkk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80...") == 0);
|
|
assert(strlen(narrow) == 22);
|
|
assert(strcmp(body, "hi") == 0);
|
|
|
|
// Same shape one char shorter, so the budget already sits on a boundary: the back-off
|
|
// must not eat a whole character that fits.
|
|
const char* utf8_fits = "Ouderkerk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80: hi";
|
|
body = ChannelSenderSplit::split(utf8_fits, narrow, sizeof narrow);
|
|
assert(strcmp(narrow, "Ouderkerk\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80\xE2\x98\x80...") == 0);
|
|
assert(strlen(narrow) == 24);
|
|
assert(strcmp(body, "hi") == 0);
|
|
|
|
// Destination too small to hold a marker: truncate plainly rather than spending the
|
|
// whole label on dots.
|
|
char tiny[5];
|
|
body = ChannelSenderSplit::split("nickname: body", tiny, sizeof tiny);
|
|
assert(strcmp(tiny, "nick") == 0);
|
|
assert(strcmp(body, "body") == 0);
|
|
|
|
// ---- Nothing to split --------------------------------------------------------------
|
|
|
|
// No author prefix — text passes through untouched, sender stays empty so the
|
|
// caller falls back to from_name.
|
|
const char* plain = "just a message";
|
|
body = ChannelSenderSplit::split(plain, sender, sizeof sender);
|
|
assert(sender[0] == '\0');
|
|
assert(body == plain);
|
|
|
|
// A bare colon without the space is not a separator.
|
|
const char* no_space = "nick:hello";
|
|
body = ChannelSenderSplit::split(no_space, sender, sizeof sender);
|
|
assert(sender[0] == '\0');
|
|
assert(body == no_space);
|
|
|
|
// Leading separator: empty author is not a name.
|
|
const char* leading = ": body";
|
|
body = ChannelSenderSplit::split(leading, sender, sizeof sender);
|
|
assert(sender[0] == '\0');
|
|
assert(body == leading);
|
|
|
|
// First separator wins — a colon in the body does not re-split.
|
|
body = ChannelSenderSplit::split("nick: ratio 3: 1", sender, sizeof sender);
|
|
assert(strcmp(sender, "nick") == 0);
|
|
assert(strcmp(body, "ratio 3: 1") == 0);
|
|
|
|
// Degenerate inputs.
|
|
assert(strcmp(ChannelSenderSplit::split(nullptr, sender, sizeof sender), "") == 0);
|
|
assert(sender[0] == '\0');
|
|
const char* keep = "nick: hi";
|
|
assert(ChannelSenderSplit::split(keep, nullptr, 0) == keep);
|
|
return 0;
|
|
}
|