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:
co-authored by
Claude
parent
fa87730e7a
commit
b8dee13d0b
4 files changed
+205
-12
No files matched your search
@@ -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:"
|
||||
|
||||
@@ -38,6 +38,10 @@ class NotificationRenderer
|
||||
static uint8_t alertBannerLineCount;
|
||||
static BannerFont alertBannerLineFonts[MAX_LINES + 1];
|
||||
static void parseBannerMessageWithFonts(const char *message);
|
||||
// Decide what text and font a banner line actually renders with: parsed (tag-stripped)
|
||||
// line if the cache covers it, otherwise the raw line with any leading font tag stripped
|
||||
// on the fly. Exposed for unit tests.
|
||||
static const char *resolveBannerLine(uint16_t lineIndex, const char *rawLine, BannerFont &lineFont);
|
||||
static void resetBanner();
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
@@ -1 +1 @@
|
||||
44
|
||||
45
|
||||
@@ -0,0 +1,176 @@
|
||||
// Regression tests for the alert-banner font-tag pipeline ([S]/[M]/[L] line prefixes).
|
||||
//
|
||||
// The BLE pairing banner (src/platform/nrf52/NRF52Bluetooth.cpp) sends
|
||||
// "Bluetooth\nPIN\n[M]<pin>" with notification type pairing_pin. The [M] prefix is a
|
||||
// font-change tag, never text: it must be stripped by parseBannerMessageWithFonts and,
|
||||
// crucially, must also be stripped when a draw resolves a line the parsed cache doesn't
|
||||
// cover (the shape of the historical bug where the pairing PIN rendered a literal "[M]").
|
||||
#include "MeshTypes.h" // Include BEFORE TestUtil.h (provides NodeNum, etc.)
|
||||
#include "TestUtil.h" // initializeTestEnvironment()
|
||||
#include <unity.h>
|
||||
|
||||
#if HAS_SCREEN // Same guard as the module under test
|
||||
|
||||
#include "graphics/draw/NotificationRenderer.h"
|
||||
#include <cstring>
|
||||
|
||||
using graphics::NotificationRenderer;
|
||||
using graphics::notificationTypeEnum;
|
||||
|
||||
static const char *BLE_PIN_MESSAGE = "Bluetooth\nPIN\n[M]123 456";
|
||||
|
||||
// Reset every static the tests touch, so each case starts from a known state.
|
||||
void setUp(void)
|
||||
{
|
||||
NotificationRenderer::alertBannerMessage[0] = '\0';
|
||||
NotificationRenderer::parseBannerMessageWithFonts("");
|
||||
NotificationRenderer::alertBannerOptions = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
}
|
||||
|
||||
void tearDown(void) {}
|
||||
|
||||
// Simulate Screen::showOverlayBanner storing and parsing a banner message.
|
||||
static void showBanner(const char *message, notificationTypeEnum type, uint8_t options = 0)
|
||||
{
|
||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0';
|
||||
NotificationRenderer::parseBannerMessageWithFonts(NotificationRenderer::alertBannerMessage);
|
||||
NotificationRenderer::alertBannerOptions = options;
|
||||
NotificationRenderer::current_notification_type = type;
|
||||
}
|
||||
|
||||
// --- parseBannerMessageWithFonts ---
|
||||
|
||||
void test_pairing_message_parses_and_strips_medium_tag()
|
||||
{
|
||||
showBanner(BLE_PIN_MESSAGE, notificationTypeEnum::pairing_pin);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(3, NotificationRenderer::alertBannerLineCount);
|
||||
TEST_ASSERT_EQUAL_STRING("Bluetooth", NotificationRenderer::alertBannerLines[0]);
|
||||
TEST_ASSERT_EQUAL_STRING("PIN", NotificationRenderer::alertBannerLines[1]);
|
||||
TEST_ASSERT_EQUAL_STRING("123 456", NotificationRenderer::alertBannerLines[2]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_DEFAULT, NotificationRenderer::alertBannerLineFonts[0]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_DEFAULT, NotificationRenderer::alertBannerLineFonts[1]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_MEDIUM, NotificationRenderer::alertBannerLineFonts[2]);
|
||||
}
|
||||
|
||||
void test_small_and_large_tags_parse()
|
||||
{
|
||||
showBanner("[S]small\n[L]large", notificationTypeEnum::text_banner);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(2, NotificationRenderer::alertBannerLineCount);
|
||||
TEST_ASSERT_EQUAL_STRING("small", NotificationRenderer::alertBannerLines[0]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_SMALL, NotificationRenderer::alertBannerLineFonts[0]);
|
||||
TEST_ASSERT_EQUAL_STRING("large", NotificationRenderer::alertBannerLines[1]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_LARGE, NotificationRenderer::alertBannerLineFonts[1]);
|
||||
}
|
||||
|
||||
void test_unknown_tag_is_kept_as_text()
|
||||
{
|
||||
showBanner("[X]hello", notificationTypeEnum::text_banner);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("[X]hello", NotificationRenderer::alertBannerLines[0]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_DEFAULT, NotificationRenderer::alertBannerLineFonts[0]);
|
||||
}
|
||||
|
||||
void test_tag_not_at_line_start_is_kept_as_text()
|
||||
{
|
||||
showBanner("PIN [M]x", notificationTypeEnum::text_banner);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("PIN [M]x", NotificationRenderer::alertBannerLines[0]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_DEFAULT, NotificationRenderer::alertBannerLineFonts[0]);
|
||||
}
|
||||
|
||||
void test_tag_only_line_yields_empty_text_with_font()
|
||||
{
|
||||
showBanner("[L]", notificationTypeEnum::text_banner);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("", NotificationRenderer::alertBannerLines[0]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_LARGE, NotificationRenderer::alertBannerLineFonts[0]);
|
||||
}
|
||||
|
||||
void test_lone_bracket_line_is_kept_as_text()
|
||||
{
|
||||
showBanner("[", notificationTypeEnum::text_banner);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("[", NotificationRenderer::alertBannerLines[0]);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_DEFAULT, NotificationRenderer::alertBannerLineFonts[0]);
|
||||
}
|
||||
|
||||
// --- resolveBannerLine: what the draw code actually puts on the panel ---
|
||||
|
||||
void test_resolve_uses_parsed_lines_for_pairing_pin()
|
||||
{
|
||||
showBanner(BLE_PIN_MESSAGE, notificationTypeEnum::pairing_pin);
|
||||
|
||||
NotificationRenderer::BannerFont font = NotificationRenderer::BANNER_FONT_DEFAULT;
|
||||
const char *text = NotificationRenderer::resolveBannerLine(2, "[M]123 456", font);
|
||||
TEST_ASSERT_EQUAL_STRING("123 456", text);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_MEDIUM, font);
|
||||
}
|
||||
|
||||
// The historical bug: the pairing banner drawn from the raw message, with the parsed-line
|
||||
// cache not consulted (before the pairing_pin type was tag-aware) or not populated (a draw
|
||||
// racing the parse from the BLE task). The tag must still act as a font change, not text.
|
||||
void test_resolve_strips_tag_when_parsed_cache_missing()
|
||||
{
|
||||
strncpy(NotificationRenderer::alertBannerMessage, BLE_PIN_MESSAGE, 255);
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::pairing_pin;
|
||||
NotificationRenderer::alertBannerOptions = 0;
|
||||
// Deliberately no parseBannerMessageWithFonts call: cache empty.
|
||||
|
||||
NotificationRenderer::BannerFont font = NotificationRenderer::BANNER_FONT_DEFAULT;
|
||||
const char *text = NotificationRenderer::resolveBannerLine(2, "[M]123 456", font);
|
||||
TEST_ASSERT_EQUAL_STRING("123 456", text);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_MEDIUM, font);
|
||||
}
|
||||
|
||||
// Picker content can be user data (e.g. node names); it must never be tag-interpreted.
|
||||
void test_resolve_leaves_picker_lines_untouched()
|
||||
{
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker;
|
||||
NotificationRenderer::alertBannerOptions = 0;
|
||||
|
||||
NotificationRenderer::BannerFont font = NotificationRenderer::BANNER_FONT_LARGE;
|
||||
const char *text = NotificationRenderer::resolveBannerLine(0, "[M]allory", font);
|
||||
TEST_ASSERT_EQUAL_STRING("[M]allory", text);
|
||||
TEST_ASSERT_EQUAL(NotificationRenderer::BANNER_FONT_DEFAULT, font);
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(test_pairing_message_parses_and_strips_medium_tag);
|
||||
RUN_TEST(test_small_and_large_tags_parse);
|
||||
RUN_TEST(test_unknown_tag_is_kept_as_text);
|
||||
RUN_TEST(test_tag_not_at_line_start_is_kept_as_text);
|
||||
RUN_TEST(test_tag_only_line_yields_empty_text_with_font);
|
||||
RUN_TEST(test_lone_bracket_line_is_kept_as_text);
|
||||
|
||||
RUN_TEST(test_resolve_uses_parsed_lines_for_pairing_pin);
|
||||
RUN_TEST(test_resolve_strips_tag_when_parsed_cache_missing);
|
||||
RUN_TEST(test_resolve_leaves_picker_lines_untouched);
|
||||
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
|
||||
#else // !HAS_SCREEN
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
UNITY_BEGIN();
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
|
||||
#endif // HAS_SCREEN
|
||||
Reference in New Issue
Block a user