Merge pull request #185 from ladyada-eagleclaw/main

Add bounded NMEA field iteration
This commit is contained in:
Limor "Ladyada" Fried authored and GitHub committed 2026-09-15 15:20:34 -04:00
commit afabd2d182
3 files changed
+140

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
/* Host regression for bounded field iteration. From the repository root:
g++ -std=c++11 -Wall -Wextra -Werror -fsanitize=address,undefined -Isrc \
src/Adafruit_NMEA.cpp extras/tests/nmea_fields/nmea_fields.cpp \
-o /tmp/nmea_fields
/tmp/nmea_fields
*/
#include "Adafruit_NMEA.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static void expectField(nmea_span_t &remaining, const char *expected);
static void expectEnd(nmea_span_t &remaining);
int main() {
// No NUL terminator: AddressSanitizer catches reading beyond this array.
char text[] = {'A', ',', ',', 'B', ','};
nmea_span_t remaining = {text, sizeof(text)};
expectField(remaining, "A");
expectField(remaining, "");
expectField(remaining, "B");
expectField(remaining, "");
expectEnd(remaining);
expectEnd(remaining);
assert(memcmp(text, "A,,B,", sizeof(text)) == 0);
puts("PASS: nonterminated input, empty fields, and repeated end-of-fields");
const char commas[] = {',', ','};
remaining = {commas, sizeof(commas)};
expectField(remaining, "");
expectField(remaining, "");
expectField(remaining, "");
expectEnd(remaining);
puts("PASS: leading, consecutive, and trailing empty fields");
nmea_sentence_t sentence =
Adafruit_NMEA::validate("$PQTMVERNO*58", sizeof("$PQTMVERNO*58") - 1);
assert(sentence.status == NMEA_FRAME_VALID);
remaining = sentence.fields;
expectEnd(remaining);
sentence =
Adafruit_NMEA::validate("$PQTMVERNO,*74", sizeof("$PQTMVERNO,*74") - 1);
assert(sentence.status == NMEA_FRAME_VALID);
remaining = sentence.fields;
expectField(remaining, "");
expectEnd(remaining);
puts("PASS: validated zero-field and one-empty-field messages differ");
const char outside[] = "AB,CD";
remaining = {outside, 2};
expectField(remaining, "AB");
expectEnd(remaining);
remaining = {outside, 0};
expectField(remaining, "");
expectEnd(remaining);
remaining = {NULL, 100};
expectEnd(remaining);
puts("PASS: declared boundaries and absent cursors are respected");
char longText[300];
memset(longText, 'A', sizeof(longText));
remaining = {longText, sizeof(longText)};
nmea_span_t field = Adafruit_NMEA::nextField(remaining);
assert(field.data == longText && field.length == sizeof(longText));
expectEnd(remaining);
puts("PASS: fields longer than 255 bytes keep their full length");
nmea_span_t first = {"A,B", 3};
nmea_span_t second = {"1,,2", 4};
nmea_span_t saved = Adafruit_NMEA::nextField(first);
expectField(second, "1");
expectField(first, "B");
expectField(second, "");
expectEnd(first);
expectField(second, "2");
expectEnd(second);
assert(saved.length == 1 && saved.data[0] == 'A');
puts("PASS: interleaved cursors and previously returned views stay "
"independent");
return 0;
}
static void expectField(nmea_span_t &remaining, const char *expected) {
const char *start = remaining.data;
nmea_span_t field = Adafruit_NMEA::nextField(remaining);
assert(field.data != NULL && field.data == start);
assert(field.length == strlen(expected));
if (field.length)
assert(memcmp(field.data, expected, field.length) == 0);
}
static void expectEnd(nmea_span_t &remaining) {
nmea_span_t field = Adafruit_NMEA::nextField(remaining);
assert(field.data == NULL && field.length == 0);
assert(remaining.data == NULL && remaining.length == 0);
}
+42
View File
@@ -109,3 +109,45 @@ nmea_sentence_t Adafruit_NMEA::validate(const char *data, size_t length) {
}
return result;
}
/**************************************************************************/
/*!
@brief Return the next field and advance a bounded field cursor.
@param remaining Start with a copy of a VALID sentence's fields span.
Updated in place to refer to the fields after the next comma, or to an
absent span after the last field. Its input storage must remain readable.
@return Borrowed field text, excluding the comma. Non-NULL data with zero
length means an empty field; NULL data means there are no fields left.
Leading, consecutive, and trailing commas preserve empty fields. An absent
cursor stays absent on subsequent calls. No text is copied or modified,
and no NUL terminator is required. Only the supplied length is inspected.
Iterate each cursor in order for one forward scan of its fields.
To mark a cursor exhausted, set remaining.data to NULL. Setting only
remaining.length to zero still yields one empty field if data is non-NULL.
*/
/**************************************************************************/
nmea_span_t Adafruit_NMEA::nextField(nmea_span_t &remaining) {
nmea_span_t field = {NULL, 0};
if (!remaining.data) {
remaining.length = 0;
return field;
}
field.data = remaining.data;
while (field.length < remaining.length &&
remaining.data[field.length] != ',') {
field.length++;
}
if (field.length < remaining.length) {
// Keep a non-NULL cursor after a trailing comma: one empty field remains.
remaining.data += field.length + 1;
remaining.length -= field.length + 1;
} else {
remaining.data = NULL;
remaining.length = 0;
}
return field;
}
+1
View File
@@ -47,6 +47,7 @@ typedef struct {
class Adafruit_NMEA {
public:
static nmea_sentence_t validate(const char *data, size_t length);
static nmea_span_t nextField(nmea_span_t &remaining);
};
#endif // ADAFRUIT_NMEA_H