mirror of
https://github.com/adafruit/Adafruit_GPS.git
synced 2026-10-26 22:10:17 +00:00
Merge pull request #184 from ladyada-eagleclaw/main
Add bounded NMEA sentence validation
This commit is contained in:
4 files changed
+416
No files matched your search
@@ -0,0 +1,148 @@
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@file Adafruit_NMEA.h
|
||||
@brief Proposed hardware-independent NMEA API for review.
|
||||
|
||||
Declarations only: this proposal is not implemented or installed from src/.
|
||||
Private storage and class layout will be defined with the implementation.
|
||||
Existing Adafruit_GPS code does not use this header.
|
||||
|
||||
Intended inheritance:
|
||||
Adafruit_NMEA -> Adafruit_GNSS -> Adafruit_GPS (existing MTK API)
|
||||
-> Adafruit_LC29H (Quectel API)
|
||||
|
||||
Written for Adafruit Industries. BSD license; see license.txt.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef ADAFRUIT_NMEA_H
|
||||
#define ADAFRUIT_NMEA_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** Result of framing or validating a sentence. Recognition belongs to GNSS
|
||||
* or receiver-specific decoders: an unknown address can still be VALID. */
|
||||
typedef enum : uint8_t {
|
||||
NMEA_FRAME_INCOMPLETE, ///< More bytes are needed; no new sentence is ready.
|
||||
NMEA_FRAME_VALID, ///< Complete format and checksum are valid.
|
||||
NMEA_FRAME_BAD_FORMAT, ///< Invalid argument, address, framing, or trailer.
|
||||
NMEA_FRAME_BAD_CHECKSUM, ///< Two hex checksum digits do not match the
|
||||
///< payload.
|
||||
NMEA_FRAME_OVERFLOW ///< Receive buffer has no room for the whole sentence.
|
||||
} nmea_frame_status_t;
|
||||
|
||||
/** Borrowed text with an explicit length; not necessarily NUL-terminated.
|
||||
* NULL data means absent. Non-NULL data with length zero means present/empty.
|
||||
* The owner must keep the underlying storage alive and unchanged while used.
|
||||
*/
|
||||
typedef struct {
|
||||
const char *data; ///< First character, or NULL for an absent field.
|
||||
size_t length; ///< Number of characters, excluding any NUL terminator.
|
||||
} nmea_span_t;
|
||||
|
||||
/** Sentence views. Only text is available when a complete sentence is invalid;
|
||||
* address and fields require VALID status. No text is copied or whitelisted.
|
||||
*/
|
||||
typedef struct {
|
||||
nmea_frame_status_t status; ///< Result of validation.
|
||||
nmea_span_t text; ///< Entire supplied sentence, including its trailer.
|
||||
nmea_span_t address; ///< Full address, e.g. GNGGA, PAIR001, or PQTMVERNO.
|
||||
nmea_span_t fields; ///< Text after the first comma and before '*'.
|
||||
} nmea_sentence_t;
|
||||
|
||||
/// Result of converting one complete field to a decimal number.
|
||||
typedef enum : uint8_t {
|
||||
NMEA_NUMBER_VALID, ///< The complete field is a representable decimal.
|
||||
NMEA_NUMBER_MISSING, ///< Field is absent.
|
||||
NMEA_NUMBER_EMPTY, ///< Field is present but empty.
|
||||
NMEA_NUMBER_BAD_FORMAT, ///< Invalid character or decimal syntax.
|
||||
NMEA_NUMBER_OUT_OF_RANGE ///< Coefficient or decimal-place count overflowed.
|
||||
} nmea_number_status_t;
|
||||
|
||||
/** Exact decimal value: coefficient / 10^decimalPlaces, with no float step.
|
||||
* Coordinate interpretation and conversion to degrees belong to Adafruit_GNSS.
|
||||
* On any conversion failure, coefficient and decimalPlaces are both zero. */
|
||||
typedef struct {
|
||||
nmea_number_status_t status; ///< Result of conversion.
|
||||
int64_t coefficient; ///< Signed integer containing the decimal digits.
|
||||
uint8_t decimalPlaces; ///< Number of digits following the decimal point.
|
||||
} nmea_decimal_t;
|
||||
|
||||
/** Proposed framing and field utilities shared by receiver classes.
|
||||
*
|
||||
* No transport, GPS fix data, command waits, heap allocation, or field arrays.
|
||||
* Framing state and timestamps belong to each instance. Supplied-buffer
|
||||
* validation, field iteration, conversion, and command building are stateless.
|
||||
* One caller feeds each instance; concurrent access requires synchronization.
|
||||
*/
|
||||
class Adafruit_NMEA {
|
||||
public:
|
||||
// Borrow two non-overlapping buffers, each capacity bytes including NUL.
|
||||
// They must outlive this object. This permits reuse of GPS's existing pair.
|
||||
// Invalid storage makes feed() return BAD_FORMAT without writing to it.
|
||||
Adafruit_NMEA(char *firstBuffer, char *secondBuffer, size_t capacity);
|
||||
|
||||
// Copies must not share writable receive buffers with independent state.
|
||||
Adafruit_NMEA(const Adafruit_NMEA &) = delete;
|
||||
Adafruit_NMEA &operator=(const Adafruit_NMEA &) = delete;
|
||||
|
||||
// Drop receive state and invalidate all views returned by lastSentence().
|
||||
void reset();
|
||||
|
||||
// Supply one byte and its receive time in milliseconds (e.g. millis()).
|
||||
// Ignore bytes before '$' or '!'; a new start character restarts assembly.
|
||||
// LF completes the sentence; its format and checksum determine the result.
|
||||
// Overflow discards that sentence and resumes at the next start character.
|
||||
// Each complete, non-overflowed line replaces lastSentence(), even if
|
||||
// invalid. Retaining invalid raw text permits the existing lastNMEA()/logger
|
||||
// behavior.
|
||||
nmea_frame_status_t feed(uint8_t byte, uint32_t receivedAtMs);
|
||||
|
||||
// Return the latest complete line, or INCOMPLETE/absent spans before one.
|
||||
// Views expire on the next complete line, reset(), or destruction.
|
||||
// Process each line promptly: there is no queue of older sentences.
|
||||
nmea_sentence_t lastSentence() const;
|
||||
|
||||
// Time of the '$' or '!' of the latest complete sentence; zero before one
|
||||
// exists. Zero is also a valid timestamp; use lastSentence().status to tell
|
||||
// them apart.
|
||||
uint32_t sentenceStartedAt() const;
|
||||
|
||||
// Time of the LF of that same sentence; unsigned times wrap like millis().
|
||||
uint32_t sentenceReceivedAt() const;
|
||||
|
||||
// Validate exactly length bytes, excluding NUL; never modify or overread
|
||||
// data. Accept '$' or '!', a nonempty ASCII alphanumeric address, optional
|
||||
// fields,
|
||||
// '*', two hex checksum digits, then optional CR followed by optional LF.
|
||||
// Reject embedded NUL/control characters and extra trailer characters.
|
||||
// Missing/invalid hex digits are BAD_FORMAT; a mismatch is BAD_CHECKSUM.
|
||||
// Zero fields means absent fields, e.g. "$PQTMVERNO*58".
|
||||
// One empty field means non-NULL fields.data with zero length.
|
||||
// NULL data yields BAD_FORMAT with all spans absent.
|
||||
// Returned spans borrow data; receive state and timestamps are unchanged.
|
||||
static nmea_sentence_t validate(const char *data, size_t length);
|
||||
|
||||
// Start with a copy of a VALID sentence's fields span. Return the next field
|
||||
// and advance remaining in place. Preserve empty and trailing empty fields.
|
||||
// After the last field, remaining and subsequent results are absent.
|
||||
// This supports one forward scan without a stored array of field offsets.
|
||||
static nmea_span_t nextField(nmea_span_t &remaining);
|
||||
|
||||
// Convert a whole field: optional '+'/'-', then digits and at most one '.'.
|
||||
// At least one digit is required; whitespace, exponents, and NaN are invalid.
|
||||
// Retain the supplied fractional digit count; never round or silently clamp.
|
||||
static nmea_decimal_t parseDecimal(nmea_span_t field);
|
||||
|
||||
// Build "$<body>*HH\r\n" plus NUL into caller-provided storage.
|
||||
// body contains an address and optional fields, without '$', '!', '*', NUL,
|
||||
// or control characters. Source/output must not overlap.
|
||||
// capacity includes NUL; return bytes excluding NUL, or zero on error.
|
||||
// On error, clear output[0] if output is non-NULL and capacity is nonzero.
|
||||
// Check arguments and capacity before writing; no heap allocation or I/O.
|
||||
static size_t buildCommand(char *output, size_t capacity, const char *body,
|
||||
size_t bodyLength);
|
||||
};
|
||||
|
||||
#endif // ADAFRUIT_NMEA_H
|
||||
@@ -0,0 +1,105 @@
|
||||
/* Host regression for the hardware-independent core. From the repository root:
|
||||
g++ -std=c++11 -Wall -Wextra -Werror -fsanitize=address,undefined -Isrc \
|
||||
src/Adafruit_NMEA.cpp extras/tests/nmea_validate/nmea_validate.cpp \
|
||||
-o /tmp/nmea_validate
|
||||
/tmp/nmea_validate
|
||||
*/
|
||||
|
||||
#include "Adafruit_NMEA.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void expect(const char *data, size_t length, nmea_frame_status_t status);
|
||||
static void expectText(nmea_span_t span, const char *text);
|
||||
|
||||
int main() {
|
||||
const char *valid[] = {
|
||||
"$A*41", "$A*41\r", "$A*41\n", "$A*41\r\n",
|
||||
"!A*41", "$PQTMVERNO*58", "$PAIR001,004,0*3f", "$A, *4D"};
|
||||
for (const char *text : valid)
|
||||
expect(text, strlen(text), NMEA_FRAME_VALID);
|
||||
puts("PASS: checksums, line endings, unknown and proprietary addresses");
|
||||
|
||||
nmea_sentence_t sentence =
|
||||
Adafruit_NMEA::validate("$PQTMVERNO*58", sizeof("$PQTMVERNO*58") - 1);
|
||||
expectText(sentence.address, "PQTMVERNO");
|
||||
assert(sentence.fields.data == NULL && sentence.fields.length == 0);
|
||||
sentence =
|
||||
Adafruit_NMEA::validate("$PQTMVERNO,*74", sizeof("$PQTMVERNO,*74") - 1);
|
||||
assert(sentence.status == NMEA_FRAME_VALID);
|
||||
assert(sentence.fields.data != NULL && sentence.fields.length == 0);
|
||||
sentence = Adafruit_NMEA::validate("$A,,*41", 7);
|
||||
assert(sentence.status == NMEA_FRAME_VALID);
|
||||
expectText(sentence.fields, ",");
|
||||
puts("PASS: absent, empty, and trailing empty fields stay distinct");
|
||||
|
||||
const char *malformed[] = {
|
||||
"", "$", "$A", "$A*", "$A*4", "$A*4Z",
|
||||
"$A*Z1", "$A*+1", "$A*411", "$A*41junk", "$A*41\n\r", "$A*41\r\r",
|
||||
"$A*41\n\n", "$*00", "$,*2C", "$A B*23", "$A,*", "$A**41",
|
||||
"$A,$*49", "$A,!*4C", "$A,\t*64", "A*41", "$A*41$B*42"};
|
||||
for (const char *text : malformed)
|
||||
expect(text, strlen(text), NMEA_FRAME_BAD_FORMAT);
|
||||
expect("$PQTMVERNO*59", sizeof("$PQTMVERNO*59") - 1, NMEA_FRAME_BAD_CHECKSUM);
|
||||
expect(NULL, 0, NMEA_FRAME_BAD_FORMAT);
|
||||
expect(NULL, 100, NMEA_FRAME_BAD_FORMAT);
|
||||
const char embeddedNul[] = {'$', 'A', ',', 0, '*', '6', 'D'};
|
||||
expect(embeddedNul, sizeof(embeddedNul), NMEA_FRAME_BAD_FORMAT);
|
||||
const char nonAscii[] = {'$', 'A', ',', (char)0x80, '*', 'E', 'D'};
|
||||
expect(nonAscii, sizeof(nonAscii), NMEA_FRAME_BAD_FORMAT);
|
||||
puts("PASS: malformed framing, checksum digits, control bytes, and NULL "
|
||||
"input");
|
||||
|
||||
const char fix[] =
|
||||
"$GPGGA,120009,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*40\r\n";
|
||||
const size_t length = sizeof(fix) - 1;
|
||||
// Each allocation ends at the declared length, so AddressSanitizer detects
|
||||
// any lookahead past it. The complete checksum needs no optional CR/LF.
|
||||
for (size_t i = 0; i <= length; i++) {
|
||||
char *bounded = (char *)malloc(i ? i : 1);
|
||||
assert(bounded != NULL);
|
||||
memcpy(bounded, fix, i);
|
||||
nmea_frame_status_t status = NMEA_FRAME_BAD_FORMAT;
|
||||
if (i >= length - 2)
|
||||
status = NMEA_FRAME_VALID;
|
||||
expect(bounded, i, status);
|
||||
assert(memcmp(bounded, fix, i) == 0);
|
||||
free(bounded);
|
||||
}
|
||||
puts("PASS: every truncated prefix and exact-length nonterminated input");
|
||||
|
||||
sentence = Adafruit_NMEA::validate(fix, length);
|
||||
expectText(sentence.address, "GPGGA");
|
||||
expectText(sentence.fields,
|
||||
"120009,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,");
|
||||
nmea_sentence_t other =
|
||||
Adafruit_NMEA::validate("$PQTMVERNO*58", sizeof("$PQTMVERNO*58") - 1);
|
||||
expectText(other.address, "PQTMVERNO");
|
||||
expectText(sentence.address, "GPGGA");
|
||||
assert(sentence.text.data == fix && sentence.text.length == length);
|
||||
puts("PASS: views borrow input and independent results do not overwrite it");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void expect(const char *data, size_t length,
|
||||
nmea_frame_status_t status) {
|
||||
nmea_sentence_t result = Adafruit_NMEA::validate(data, length);
|
||||
if (result.status != status) {
|
||||
fprintf(stderr, "FAIL: length %zu, expected status %d, got %d\n", length,
|
||||
status, result.status);
|
||||
abort();
|
||||
}
|
||||
assert(result.text.data == data);
|
||||
assert(result.text.length == (data ? length : 0));
|
||||
if (status != NMEA_FRAME_VALID) {
|
||||
assert(result.address.data == NULL && result.address.length == 0);
|
||||
assert(result.fields.data == NULL && result.fields.length == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void expectText(nmea_span_t span, const char *text) {
|
||||
assert(span.data != NULL && span.length == strlen(text));
|
||||
assert(memcmp(span.data, text, span.length) == 0);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@file Adafruit_NMEA.cpp
|
||||
@brief Hardware-independent NMEA sentence validation.
|
||||
|
||||
Written for Adafruit Industries. BSD license; see license.txt.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
|
||||
#include "Adafruit_NMEA.h"
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Validate a complete, length-bounded NMEA sentence without copying it.
|
||||
@param data Readable input buffer; no NUL terminator is required.
|
||||
@param length Number of bytes to inspect, excluding any NUL terminator.
|
||||
@return Status and borrowed spans. Address and fields are absent on failure.
|
||||
Raw text remains available unless data is NULL, which gives absent spans.
|
||||
|
||||
Accepts '$' or '!', an ASCII alphanumeric address, optional comma-separated
|
||||
fields, '*', two hex checksum digits, optional CR, and optional LF, in that
|
||||
order. A valid but unknown address is accepted. Embedded start markers,
|
||||
control characters, non-ASCII bytes, and extra trailer bytes are rejected.
|
||||
Missing or invalid hex digits are BAD_FORMAT; a mismatch is BAD_CHECKSUM.
|
||||
|
||||
A zero-field message such as "$PQTMVERNO*58" has absent fields. A comma
|
||||
immediately followed by '*' denotes one empty field, with non-NULL data
|
||||
and zero length. Returned views require the input to remain unchanged.
|
||||
This function allocates no memory and has no shared or per-instance state.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
nmea_sentence_t Adafruit_NMEA::validate(const char *data, size_t length) {
|
||||
nmea_sentence_t result = {
|
||||
NMEA_FRAME_BAD_FORMAT, {NULL, 0}, {NULL, 0}, {NULL, 0}};
|
||||
if (!data)
|
||||
return result;
|
||||
result.text.data = data;
|
||||
result.text.length = length;
|
||||
|
||||
// The shortest sentence has a start, one address character, '*', and two hex
|
||||
// digits. Check the length before reading even the start character.
|
||||
if (length < 5 || (data[0] != '$' && data[0] != '!'))
|
||||
return result;
|
||||
|
||||
size_t addressEnd = 0;
|
||||
size_t checksumOffset = 0;
|
||||
uint8_t checksum = 0;
|
||||
for (size_t i = 1; i < length; i++) {
|
||||
uint8_t c = (uint8_t)data[i];
|
||||
if (c == '*') {
|
||||
checksumOffset = i;
|
||||
break;
|
||||
}
|
||||
if (c < ' ' || c > '~' || c == '$' || c == '!')
|
||||
return result;
|
||||
|
||||
if (!addressEnd) {
|
||||
if (c == ',') {
|
||||
addressEnd = i;
|
||||
} else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9'))) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
checksum ^= c;
|
||||
}
|
||||
|
||||
if (!checksumOffset)
|
||||
return result;
|
||||
if (!addressEnd)
|
||||
addressEnd = checksumOffset;
|
||||
if (addressEnd == 1 || length - checksumOffset < 3)
|
||||
return result;
|
||||
|
||||
// Decode both checksum digits only after proving that both are in bounds.
|
||||
uint8_t expected = 0;
|
||||
for (size_t i = checksumOffset + 1; i < checksumOffset + 3; i++) {
|
||||
char c = data[i];
|
||||
uint8_t digit;
|
||||
if (c >= '0' && c <= '9')
|
||||
digit = c - '0';
|
||||
else if (c >= 'A' && c <= 'F')
|
||||
digit = c - 'A' + 10;
|
||||
else if (c >= 'a' && c <= 'f')
|
||||
digit = c - 'a' + 10;
|
||||
else
|
||||
return result;
|
||||
expected = expected * 16 + digit;
|
||||
}
|
||||
|
||||
size_t end = checksumOffset + 3;
|
||||
if (end < length && data[end] == '\r')
|
||||
end++;
|
||||
if (end < length && data[end] == '\n')
|
||||
end++;
|
||||
if (end != length)
|
||||
return result;
|
||||
if (checksum != expected) {
|
||||
result.status = NMEA_FRAME_BAD_CHECKSUM;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.status = NMEA_FRAME_VALID;
|
||||
result.address.data = data + 1;
|
||||
result.address.length = addressEnd - 1;
|
||||
if (addressEnd < checksumOffset) {
|
||||
result.fields.data = data + addressEnd + 1;
|
||||
result.fields.length = checksumOffset - addressEnd - 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@file Adafruit_NMEA.h
|
||||
@brief Hardware-independent NMEA sentence validation.
|
||||
|
||||
Written for Adafruit Industries. BSD license; see license.txt.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef ADAFRUIT_NMEA_H
|
||||
#define ADAFRUIT_NMEA_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** Result of framing or validating a sentence. Recognition belongs to GNSS
|
||||
* or receiver-specific decoders: an unknown address can still be VALID. */
|
||||
typedef enum : uint8_t {
|
||||
NMEA_FRAME_INCOMPLETE, ///< More bytes are needed; no new sentence is ready.
|
||||
NMEA_FRAME_VALID, ///< Complete format and checksum are valid.
|
||||
NMEA_FRAME_BAD_FORMAT, ///< Invalid argument, address, framing, or trailer.
|
||||
NMEA_FRAME_BAD_CHECKSUM, ///< Two hex checksum digits do not match the
|
||||
///< payload.
|
||||
NMEA_FRAME_OVERFLOW ///< Receive buffer has no room for the whole sentence.
|
||||
} nmea_frame_status_t;
|
||||
|
||||
/** Borrowed text with an explicit length; not necessarily NUL-terminated.
|
||||
* NULL data means absent. Non-NULL data with length zero means present/empty.
|
||||
* The owner must keep the underlying storage alive and unchanged while used.
|
||||
*/
|
||||
typedef struct {
|
||||
const char *data; ///< First character, or NULL for an absent field.
|
||||
size_t length; ///< Number of characters, excluding any NUL terminator.
|
||||
} nmea_span_t;
|
||||
|
||||
/** Sentence views. Only text is available when a sentence is invalid;
|
||||
* address and fields require VALID status. No text is copied or whitelisted.
|
||||
*/
|
||||
typedef struct {
|
||||
nmea_frame_status_t status; ///< Result of validation.
|
||||
nmea_span_t text; ///< Entire supplied sentence, including its trailer.
|
||||
nmea_span_t address; ///< Full address, e.g. GNGGA, PAIR001, or PQTMVERNO.
|
||||
nmea_span_t fields; ///< Text after the first comma and before '*'.
|
||||
} nmea_sentence_t;
|
||||
|
||||
/// Hardware-independent NMEA utilities shared by receiver classes.
|
||||
class Adafruit_NMEA {
|
||||
public:
|
||||
static nmea_sentence_t validate(const char *data, size_t length);
|
||||
};
|
||||
|
||||
#endif // ADAFRUIT_NMEA_H
|
||||
Reference in new issue
Block a user