fix(BaseUI): never render banner font tags ([S]/[M]/[L]) as literal text (#11392)

The nRF52 BLE pairing banner sends "Bluetooth\nPIN\n[M]<pin>" where [M] is
a medium-font marker for the PIN line. The renderer only honored font tags
for lines covered by the parsed-line cache, so any path that draws a banner
line from the raw message - a stored-without-reparse banner, or a draw
racing the parse from the BLE task - showed a literal "[M]" ahead of the
pairing PIN.

Extract the per-line text/font decision into resolveBannerLine(): parsed
(tag-stripped) line when the cache covers it, otherwise strip a leading
font tag from the raw line on the fly and honor the font it names. Picker
content (e.g. node names) is deliberately exempt - it can be user data and
must never be tag-interpreted.

Also measure line widths on the text actually rendered (they were measured
on the raw, un-stripped string, skewing box width and centering), and check
p[1] for NUL before reading p[2] in the tag probe, which could read one
byte past the end of a line that ends in '['.

Add a native test suite (test_banner_font_tags) covering tag parsing, the
BLE pairing message, the cache-miss fallback, and the picker exemption.


Claude-Session: https://claude.ai/code/session_01CFMdkE6d8fY28Ax3hFVDMU

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Bennett
2026-08-11 00:06:00 +00:00
committed by GitHub
co-authored by Claude
parent fa87730e7a
commit b8dee13d0b
4 changed files with 205 additions and 12 deletions
+24 -11
View File
@@ -84,7 +84,7 @@ static inline graphics::NotificationRenderer::BannerFont parseFontTagPrefix(cons
{
// Tags must be at the start of the line:
// [S] small, [M] medium, [L] large
if (p && p[0] == '[' && p[2] == ']' && p[1] != '\0') {
if (p && p[0] == '[' && p[1] != '\0' && p[2] == ']') {
char t = p[1];
if (t == 'S') {
p += 3;
@@ -136,6 +136,26 @@ static inline uint8_t effectiveLineHeightForBannerLine(graphics::NotificationRen
return (height > 3) ? (height - 3) : height;
}
const char *graphics::NotificationRenderer::resolveBannerLine(uint16_t lineIndex, const char *rawLine, BannerFont &lineFont)
{
lineFont = BANNER_FONT_DEFAULT;
bool tagAware = (current_notification_type == notificationTypeEnum::text_banner ||
current_notification_type == notificationTypeEnum::pairing_pin) &&
alertBannerOptions == 0;
if (!tagAware)
return rawLine;
if (lineIndex < alertBannerLineCount) {
lineFont = alertBannerLineFonts[lineIndex];
return alertBannerLines[lineIndex];
}
// The parsed-line cache doesn't cover this line (the banner text was stored without a
// re-parse, or a draw raced the parse from another task): strip the tag here too, so it
// acts as a font change and never renders as literal text - the BLE pair PIN banner
// prefixes its PIN line with [M].
lineFont = parseFontTagPrefix(rawLine);
return rawLine;
}
void graphics::NotificationRenderer::parseBannerMessageWithFonts(const char *message)
{
alertBannerLineCount = 0;
@@ -845,9 +865,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
BannerFont lineFonts[totalLines] = {};
uint8_t lineEffectiveHeights[totalLines] = {0};
const char *renderLines[totalLines] = {0};
bool useTaggedBannerFonts = (current_notification_type == notificationTypeEnum::text_banner ||
current_notification_type == notificationTypeEnum::pairing_pin) &&
alertBannerOptions == 0;
if (maxWidth != 0)
is_picker = true;
@@ -860,12 +877,8 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
uint16_t widestLineWithBars = 0;
while (lines[lineCount] != nullptr) {
const char *renderText = lines[lineCount];
BannerFont lineFont = BANNER_FONT_DEFAULT;
if (useTaggedBannerFonts && lineCount < alertBannerLineCount) {
renderText = alertBannerLines[lineCount];
lineFont = alertBannerLineFonts[lineCount];
}
const char *renderText = resolveBannerLine(lineCount, lines[lineCount], lineFont);
renderLines[lineCount] = renderText;
lineFonts[lineCount] = lineFont;
lineEffectiveHeights[lineCount] = effectiveLineHeightForBannerLine(lineFont);
@@ -879,10 +892,10 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
if (current_notification_type == notificationTypeEnum::node_picker) {
char measureBuffer[64] = {0};
strncpy(measureBuffer, lines[lineCount], std::min<size_t>(lineLengths[lineCount], sizeof(measureBuffer) - 1));
strncpy(measureBuffer, renderText, std::min<size_t>(lineLengths[lineCount], sizeof(measureBuffer) - 1));
lineWidths[lineCount] = UIRenderer::measureStringWithEmotes(display, measureBuffer);
} else {
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
lineWidths[lineCount] = display->getStringWidth(renderText, lineLengths[lineCount], true);
}
// Consider extra width for signal bars on lines that contain "Signal:"