Files
firmware/test/test_nmea_wpl/test_main.cpp
Thomas Göttgens 6367132919 Fix the NMEA checksum offset and harden the buffer writes around it (#11293)
* Checksum NMEA sentences from the $ delimiter

The PositionLite printWPL() format begins with a CRLF, so the fixed start offset of 1 folded the newline and the $ into the checksum and every sentence went out with a wrong value. Locate the $ instead and stop at the terminator or a \*.

* Clamp truncated writes and harden the remaining fixed buffers

snprintf returns the length it would have written, so a truncated NMEA sentence
made buf + len point past the buffer and bufsz - len underflow into a huge size
for the checksum append. Clamp after each write.

Also pulls in the rest of #11236: the two remaining Dropzone sprintf calls, the
dead strcpy in mt_sprintf that wrote one byte past a zero-size allocation for an
empty format, and the 10-byte errcode buffer that INT32_MIN overflows.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Bail out on a zero-sized buffer and cast err for %ld

snprintf writes nothing at all when bufsz is 0, not even a terminator, so the
checksum helper would run strchr over whatever the buffer already held. Return
before touching it.

int32_t is not long on every target, so cast before formatting with %ld.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Add NMEA sentence regression tests

Covers checksum computation from the $ delimiter for both printWPL
overloads and printGGA, zero-sized buffers, and truncated buffers down
to one byte.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Tighten checksum parsing and pin the WPL fixture checksum

Require exactly two hex digits followed by the sentence terminator, and
assert both WPL overloads against a known checksum instead of comparing
them to each other.

* Bump native suite count to 43

---------

Co-authored-by: Andrew Yong <me@ndoo.sg>
2026-07-31 08:20:56 +00:00

159 lines
4.4 KiB
C++

#include "GeoCoord.h"
#include "NMEAWPL.h"
#include "TestUtil.h"
#include "mesh-pb-constants.h"
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <unity.h>
void setUp(void) {}
void tearDown(void) {}
static meshtastic_PositionLite makePositionLite()
{
meshtastic_PositionLite pos = meshtastic_PositionLite_init_default;
pos.latitude_i = 472852133;
pos.longitude_i = 85652500;
pos.altitude = 400;
pos.time = 42;
return pos;
}
static meshtastic_Position makePosition()
{
meshtastic_Position pos = meshtastic_Position_init_default;
pos.has_latitude_i = true;
pos.latitude_i = 472852133;
pos.has_longitude_i = true;
pos.longitude_i = 85652500;
pos.has_altitude = true;
pos.altitude = 400;
pos.timestamp = 43;
return pos;
}
static uint32_t expectedChecksum(const char *sentence)
{
uint32_t chk = 0;
const char *c = strchr(sentence, '$');
TEST_ASSERT_NOT_NULL(c);
for (c++; *c && *c != '*'; c++)
chk ^= (uint8_t)*c;
return chk;
}
static uint32_t emittedChecksum(const char *buf)
{
const char *star = strrchr(buf, '*');
TEST_ASSERT_NOT_NULL(star);
TEST_ASSERT_TRUE(isxdigit((unsigned char)star[1]));
TEST_ASSERT_TRUE(isxdigit((unsigned char)star[2]));
TEST_ASSERT_TRUE(star[3] == '\r' || star[3] == '\0');
unsigned parsed = 0;
TEST_ASSERT_EQUAL_INT(1, sscanf(star + 1, "%02X", &parsed));
return parsed;
}
static void assertChecksumMatchesBody(const char *buf)
{
TEST_ASSERT_EQUAL_UINT32(expectedChecksum(buf), emittedChecksum(buf));
}
void test_wpl_lite_checksum_skips_leading_crlf(void)
{
char buf[128];
meshtastic_PositionLite pos = makePositionLite();
uint32_t len = printWPL(buf, sizeof(buf), pos, "Test", false);
TEST_ASSERT_TRUE(len < sizeof(buf));
TEST_ASSERT_EQUAL_CHAR('\r', buf[0]);
TEST_ASSERT_EQUAL_CHAR('\n', buf[1]);
TEST_ASSERT_EQUAL_CHAR('$', buf[2]);
assertChecksumMatchesBody(buf);
}
void test_wpl_position_checksum(void)
{
char buf[128];
meshtastic_Position pos = makePosition();
uint32_t len = printWPL(buf, sizeof(buf), pos, "Test", false);
TEST_ASSERT_TRUE(len < sizeof(buf));
TEST_ASSERT_EQUAL_CHAR('$', buf[0]);
assertChecksumMatchesBody(buf);
}
void test_gga_checksum(void)
{
char buf[160];
meshtastic_Position pos = makePosition();
uint32_t len = printGGA(buf, sizeof(buf), pos);
TEST_ASSERT_TRUE(len < sizeof(buf));
TEST_ASSERT_EQUAL_CHAR('$', buf[0]);
assertChecksumMatchesBody(buf);
}
void test_crlf_prefix_does_not_change_checksum(void)
{
const uint32_t fixtureChecksum = 0x69;
char withPrefix[128];
char withoutPrefix[128];
meshtastic_PositionLite lite = makePositionLite();
meshtastic_Position pos = makePosition();
printWPL(withPrefix, sizeof(withPrefix), lite, "Test", false);
printWPL(withoutPrefix, sizeof(withoutPrefix), pos, "Test", false);
TEST_ASSERT_EQUAL_UINT32(fixtureChecksum, emittedChecksum(withPrefix));
TEST_ASSERT_EQUAL_UINT32(fixtureChecksum, emittedChecksum(withoutPrefix));
}
void test_zero_sized_buffer_writes_nothing(void)
{
char buf[64];
memset(buf, 'A', sizeof(buf));
meshtastic_PositionLite lite = makePositionLite();
meshtastic_Position pos = makePosition();
TEST_ASSERT_EQUAL_UINT32(0, printWPL(buf, 0, lite, "Test", false));
TEST_ASSERT_EQUAL_UINT32(0, printWPL(buf, 0, pos, "Test", false));
TEST_ASSERT_EQUAL_UINT32(0, printGGA(buf, 0, pos));
for (size_t i = 0; i < sizeof(buf); i++)
TEST_ASSERT_EQUAL_CHAR('A', buf[i]);
}
void test_truncated_buffers_stay_in_bounds(void)
{
const size_t sizes[] = {1, 2, 8, 20, 40};
meshtastic_PositionLite lite = makePositionLite();
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++) {
char buf[128];
memset(buf, 0x7E, sizeof(buf));
uint32_t len = printWPL(buf, sizes[s], lite, "Test", false);
TEST_ASSERT_TRUE(len < sizes[s]);
for (size_t i = sizes[s]; i < sizeof(buf); i++)
TEST_ASSERT_EQUAL_HEX8(0x7E, (uint8_t)buf[i]);
}
}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_wpl_lite_checksum_skips_leading_crlf);
RUN_TEST(test_wpl_position_checksum);
RUN_TEST(test_gga_checksum);
RUN_TEST(test_crlf_prefix_does_not_change_checksum);
RUN_TEST(test_zero_sized_buffer_writes_nothing);
RUN_TEST(test_truncated_buffers_stay_in_bounds);
exit(UNITY_END());
}
void loop() {}