diff --git a/minmea.c b/minmea.c index 8bbbc59..72f1049 100644 --- a/minmea.c +++ b/minmea.c @@ -72,27 +72,45 @@ static inline bool minmea_isfield(char c) { bool minmea_scan(const char *sentence, const char *format, ...) { bool result = false; + bool optional = false; va_list ap; va_start(ap, format); const char *field = sentence; #define next_field() \ do { \ - while (minmea_isfield(*sentence++)) {} \ - field = sentence; \ + /* Progress to the next field. */ \ + while (minmea_isfield(*sentence)) \ + sentence++; \ + /* Make sure there is a field there. */ \ + if (*sentence == ',') { \ + sentence++; \ + field = sentence; \ + } else { \ + field = NULL; \ + } \ } while (0) while (*format) { char type = *format++; + if (type == ';') { + // All further fields are optional. + optional = true; + continue; + } + + if (!field && !optional) { + // Field requested but we ran out if input. Bail out. + goto parse_error; + } + switch (type) { case 'c': { // Single character field (char). char value = '\0'; - if (minmea_isfield(*field)) + if (field && minmea_isfield(*field)) value = *field; - else - value = '\0'; *va_arg(ap, char *) = value; } break; @@ -100,7 +118,7 @@ bool minmea_scan(const char *sentence, const char *format, ...) case 'd': { // Single character direction field (int). int value = 0; - if (minmea_isfield(*field)) { + if (field && minmea_isfield(*field)) { switch (*field) { case 'N': case 'E': @@ -111,7 +129,7 @@ bool minmea_scan(const char *sentence, const char *format, ...) value = -1; break; default: - goto end; + goto parse_error; } } @@ -123,27 +141,29 @@ bool minmea_scan(const char *sentence, const char *format, ...) int value = -1; int scale = 0; - while (minmea_isfield(*field)) { - if (*field == '+' && !sign && value == -1) { - sign = 1; - } else if (*field == '-' && !sign && value == -1) { - sign = -1; - } else if (isdigit((unsigned char) *field)) { - if (value == -1) - value = 0; - value = (10 * value) + (*field - '0'); - if (scale) - scale *= 10; - } else if (*field == '.' && scale == 0) { - scale = 1; - } else { - goto end; + if (field) { + while (minmea_isfield(*field)) { + if (*field == '+' && !sign && value == -1) { + sign = 1; + } else if (*field == '-' && !sign && value == -1) { + sign = -1; + } else if (isdigit((unsigned char) *field)) { + if (value == -1) + value = 0; + value = (10 * value) + (*field - '0'); + if (scale) + scale *= 10; + } else if (*field == '.' && scale == 0) { + scale = 1; + } else { + goto parse_error; + } + field++; } - field++; } if ((sign || scale) && value == -1) - goto end; + goto parse_error; if (value == -1) { value = 0; @@ -157,12 +177,14 @@ bool minmea_scan(const char *sentence, const char *format, ...) } break; case 'i': { // Integer value, default 0 (int). - int value; + int value = 0; - char *endptr; - value = strtol(field, &endptr, 10); - if (minmea_isfield(*endptr)) - goto end; + if (field) { + char *endptr; + value = strtol(field, &endptr, 10); + if (minmea_isfield(*endptr)) + goto parse_error; + } *va_arg(ap, int *) = value; } break; @@ -170,17 +192,24 @@ bool minmea_scan(const char *sentence, const char *format, ...) case 's': { // String value (char *). char *buf = va_arg(ap, char *); - while (minmea_isfield(*field)) - *buf++ = *field++; + if (field) { + while (minmea_isfield(*field)) + *buf++ = *field++; + } + *buf = '\0'; } break; case 't': { // NMEA talker+sentence identifier (char *). + // This field is always mandatory. + if (!field) + goto parse_error; + if (field[0] != '$') - goto end; + goto parse_error; for (int i=0; i<5; i++) if (!minmea_isfield(field[1+i])) - goto end; + goto parse_error; char *buf = va_arg(ap, char *); memcpy(buf, field+1, 5); @@ -191,16 +220,18 @@ bool minmea_scan(const char *sentence, const char *format, ...) struct minmea_date *date = va_arg(ap, struct minmea_date *); int d = -1, m = -1, y = -1; - // Always six digits. - for (int i=0; i<6; i++) - if (!isdigit((unsigned char) field[i])) - goto end_D; - d = strtol((char[]) {field[0], field[1], '\0'}, NULL, 10); - m = strtol((char[]) {field[2], field[3], '\0'}, NULL, 10); - y = strtol((char[]) {field[4], field[5], '\0'}, NULL, 10); + if (field && minmea_isfield(*field)) { + // Always six digits. + for (int i=0; i<6; i++) + if (!isdigit((unsigned char) field[i])) + goto parse_error; + + d = strtol((char[]) {field[0], field[1], '\0'}, NULL, 10); + m = strtol((char[]) {field[2], field[3], '\0'}, NULL, 10); + y = strtol((char[]) {field[4], field[5], '\0'}, NULL, 10); + } - end_D: date->day = d; date->month = m; date->year = y; @@ -210,30 +241,32 @@ bool minmea_scan(const char *sentence, const char *format, ...) struct minmea_time *time = va_arg(ap, struct minmea_time *); int h = -1, i = -1, s = -1, u = -1; - // Minimum required: integer time. - for (int i=0; i<6; i++) - if (!isdigit((unsigned char) field[i])) - goto end_T; - h = strtol((char[]) {field[0], field[1], '\0'}, NULL, 10); - i = strtol((char[]) {field[2], field[3], '\0'}, NULL, 10); - s = strtol((char[]) {field[4], field[5], '\0'}, NULL, 10); - field += 6; + if (field && minmea_isfield(*field)) { + // Minimum required: integer time. + for (int i=0; i<6; i++) + if (!isdigit((unsigned char) field[i])) + goto parse_error; - // Extra: fractional time. Saved as microseconds. - if (*field++ == '.') { - int value = 0; - int scale = 1000000; - while (isdigit((unsigned char) *field) && scale > 1) { - value = (value * 10) + (*field++ - '0'); - scale /= 10; + h = strtol((char[]) {field[0], field[1], '\0'}, NULL, 10); + i = strtol((char[]) {field[2], field[3], '\0'}, NULL, 10); + s = strtol((char[]) {field[4], field[5], '\0'}, NULL, 10); + field += 6; + + // Extra: fractional time. Saved as microseconds. + if (*field++ == '.') { + int value = 0; + int scale = 1000000; + while (isdigit((unsigned char) *field) && scale > 1) { + value = (value * 10) + (*field++ - '0'); + scale /= 10; + } + u = value * scale; + } else { + u = 0; } - u = value * scale; - } else { - u = 0; } - end_T: time->hours = h; time->minutes = i; time->seconds = s; @@ -244,17 +277,16 @@ bool minmea_scan(const char *sentence, const char *format, ...) } break; default: { // Unknown. - goto end; + goto parse_error; } break; } - // Advance to next field. next_field(); } result = true; -end: +parse_error: va_end(ap); return result; } diff --git a/tests.c b/tests.c index c45bde3..8ec9e3a 100644 --- a/tests.c +++ b/tests.c @@ -61,7 +61,7 @@ END_TEST START_TEST(test_minmea_scan_c) { - char ch; + char ch, extra; ck_assert(minmea_scan("A,123.45", "c", &ch) == true); ck_assert_int_eq(ch, 'A'); @@ -71,6 +71,19 @@ START_TEST(test_minmea_scan_c) ck_assert(minmea_scan(",123.45", "c", &ch) == true); ck_assert_int_eq(ch, '\0'); + + ck_assert(minmea_scan("A,B", "cc", &ch, &extra) == true); + ck_assert_int_eq(ch, 'A'); + ck_assert_int_eq(extra, 'B'); + + ck_assert(minmea_scan("C", "cc", &ch, &extra) == false); + + ck_assert(minmea_scan("D", "c;c", &ch, &extra) == true); + ck_assert_int_eq(ch, 'D'); + ck_assert_int_eq(extra, '\0'); + ck_assert(minmea_scan("E,F", "c;c", &ch, &extra) == true); + ck_assert_int_eq(ch, 'E'); + ck_assert_int_eq(extra, 'F'); } END_TEST @@ -131,6 +144,45 @@ START_TEST(test_minmea_scan_f) ck_assert(minmea_scan("-18000.00000", "f", &value, &scale) == true); ck_assert_int_eq(value, -1800000000); ck_assert_int_eq(scale, 100000); + + /* optional values */ + ck_assert(minmea_scan("foo", "_;f", &value, &scale) == true); + ck_assert_int_eq(scale, 0); + ck_assert(minmea_scan("foo,", "_;f", &value, &scale) == true); + ck_assert_int_eq(scale, 0); + ck_assert(minmea_scan("foo,12.3", "_;f", &value, &scale) == true); + ck_assert_int_eq(value, 123); + ck_assert_int_eq(scale, 10); +} +END_TEST + +START_TEST(test_minmea_scan_i) +{ + int value, extra; + + // valid parses + ck_assert(minmea_scan("14", "i", &value) == true); + ck_assert_int_eq(value, 14); + ck_assert(minmea_scan("-1234", "i", &value) == true); + ck_assert_int_eq(value, -1234); + + // empty field + ck_assert(minmea_scan("", "i", &value) == true); + ck_assert_int_eq(value, 0); + + // invalid value + ck_assert(minmea_scan("foo", "i", &value) == false); + + // missing field + ck_assert(minmea_scan("41", "ii", &value, &extra) == false); + + /* optional values */ + ck_assert(minmea_scan("10", "i;i", &value, &extra) == true); + ck_assert_int_eq(value, 10); + ck_assert(minmea_scan("20,30", "i;i", &value, &extra) == true); + ck_assert_int_eq(value, 20); + ck_assert_int_eq(extra, 30); + ck_assert(minmea_scan("42,foo", "i;i", &value, &extra) == false); } END_TEST @@ -138,9 +190,16 @@ START_TEST(test_minmea_scan_s) { char value[MINMEA_MAX_LENGTH]; + ck_assert(minmea_scan(",bar,baz", "s", value) == true); + ck_assert_str_eq(value, ""); ck_assert(minmea_scan("foo,bar,baz", "s", value) == true); ck_assert_str_eq(value, "foo"); - ck_assert(minmea_scan(",bar,baz", "s", value) == true); + + ck_assert(minmea_scan("dummy", "_;s", value) == true); + ck_assert_str_eq(value, ""); + ck_assert(minmea_scan("dummy,foo", "_;s", value) == true); + ck_assert_str_eq(value, "foo"); + ck_assert(minmea_scan("dummy,", "_;s", value) == true); ck_assert_str_eq(value, ""); } END_TEST @@ -164,6 +223,9 @@ START_TEST(test_minmea_scan_D) { struct minmea_date date; + ck_assert(minmea_scan("$GPXXX,3112", "_D", &date) == false); + ck_assert(minmea_scan("$GPXXX,foobar", "_D", &date) == false); + ck_assert(minmea_scan("$GPXXX,311299", "_D", &date) == true); ck_assert_int_eq(date.day, 31); ck_assert_int_eq(date.month, 12); @@ -180,6 +242,9 @@ START_TEST(test_minmea_scan_T) { struct minmea_time time; + ck_assert(minmea_scan("$GPXXX,2359", "_T", &time) == false); + ck_assert(minmea_scan("$GPXXX,foobar", "_T", &time) == false); + ck_assert(minmea_scan("$GPXXX,235960", "_T", &time) == true); ck_assert_int_eq(time.hours, 23); ck_assert_int_eq(time.minutes, 59); @@ -482,6 +547,7 @@ Suite *minmea_suite(void) tcase_add_test(tc_scan, test_minmea_scan_c); tcase_add_test(tc_scan, test_minmea_scan_d); tcase_add_test(tc_scan, test_minmea_scan_f); + tcase_add_test(tc_scan, test_minmea_scan_i); tcase_add_test(tc_scan, test_minmea_scan_s); tcase_add_test(tc_scan, test_minmea_scan_t); tcase_add_test(tc_scan, test_minmea_scan_D);