From: Eric S. Raymond Date: Wed, 17 Sep 2014 15:48:08 +0000 (-0400) Subject: First version of microjson module passes regression tests. X-Git-Tag: 1.0~32 X-Git-Url: http://thyrsus.com/gitweb/?a=commitdiff_plain;h=3297b0a3f24fed4a3c3315997df5f78b9da25717;p=microjson.git First version of microjson module passes regression tests. --- 3297b0a3f24fed4a3c3315997df5f78b9da25717 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..2267ff2 --- /dev/null +++ b/COPYING @@ -0,0 +1,28 @@ + BSD LICENSE + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +Neither name of the GPSD project nor the names of its contributors +may be used to endorse or promote products derived from this software +without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..59cdb5f --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +# Makefile for the microjson project: + +CFLAGS = -O + +# Add DEBUG_ENABLE for the tracing code +# CFLAGS += -DDEBUG_ENABLE + +microjson.o: microjson.c microjson.h + +test_microjson: test_microjson.o microjson.o + $(CC) -o test_microjson test_microjson.o microjson.o + +clean: + rm -f microjson.o test_json + diff --git a/README b/README new file mode 100644 index 0000000..2baa484 --- /dev/null +++ b/README @@ -0,0 +1,15 @@ + microjson README + +microjson parses a large subset of JSON directly into static C +structures without using malloc(3). + +microjson is very small and very low-footprint, intended for use in +constrained embedded environments. It may also be of interest for +code where malloc is verboten to reduce reliability and sexurity +vulnerabilities. + + Eric S. Raymond + Ocrober 2014 + + + diff --git a/microjson.c b/microjson.c new file mode 100644 index 0000000..de51418 --- /dev/null +++ b/microjson.c @@ -0,0 +1,939 @@ +/**************************************************************************** + +NAME + microjson.c - parse JSON into fixed-extent data structures + +DESCRIPTION + This module parses a large subset of JSON (JavaScript Object +Notation). Unlike more general JSON parsers, it doesn't use malloc(3) +and doesn't support polymorphism; you need to give it a set of +template structures describing the expected shape of the incoming +JSON, and it will error out if that shape is not matched. When the +parse succeeds, attribute values will be extracted into static +locations specified in the template structures. + + The "shape" of a JSON object in the type signature of its +attributes (and attribute values, and so on recursively down through +all nestings of objects and arrays). This parser is indifferent to +the order of attributes at any level, but you have to tell it in +advance what the type of each attribute value will be and where the +parsed value will be stored. The template structures may supply +default values to be used when an expected attribute is omitted. + + The preceding paragraph told one fib. A single attribute may +actually have a span of multiple specifications with different +syntactically distinguishable types (e.g. string vs. real vs. integer +vs. boolean, but not signed integer vs. unsigned integer). The parser +will match the right spec against the actual data. + + The dialect this parses has some limitations. First, it cannot +recognize the JSON "null" value. Secondly, arrays may only have +objects or strings - not reals or integers or floats - as elements +(this limitation could be easily removed if required). Third, all +elements of an array must be of the same type. + + There are separata entry points for beginning a parse of either +JSON object or a JSON array. JSON "float" quantities are actually +stored as doubles. + + This parser processes object arrays in one of two different ways, +defending on whether the array subtype is declared as object or +structobject. + + Object arrays take one base address per object subfield, and are +mapped into parallel C arrays (one per subfield). Strings are not +supported in this kind of array, as they don't have a "natural" size +to use as an offset multiplier. + + Structobjects arrays are a way to parse a list of objects to a set +of modifications to a corresponding array of C structs. The trick is +that the array object initialization has to specify both the C struct +array's base address and the stride length (the size of the C struct). +If you initialize the offset fields with the correct offsetof calls, +everything will work. Strings are supported but all string storage +has to be inline in the struct. + +PERMISSIONS + This file is Copyright (c) 2014 by the Eric S. Raymond + BSD terms apply: see the file COPYING in the distribution root for details. + +***************************************************************************/ + +/* The strptime prototype is not provided unless explicitly requested. + * We also need to set the value high enough to signal inclusion of + * newer features (like clock_gettime). See the POSIX spec for more info: + * http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_02_01_02 */ +#define _XOPEN_SOURCE 600 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "microjson.h" + +#ifdef DEBUG_ENABLE +static int debuglevel = 0; +static FILE *debugfp; + +void json_enable_debug(int level, FILE * fp) +/* control the level and destination of debug trace messages */ +{ + debuglevel = level; + debugfp = fp; +} + +static void json_trace(int errlevel, const char *fmt, ...) +/* assemble command in printf(3) style */ +{ + if (errlevel <= debuglevel) { + char buf[BUFSIZ]; + va_list ap; + + (void)strncpy(buf, "json: ", BUFSIZ-1); + buf[BUFSIZ-1] = '\0'; + va_start(ap, fmt); + (void)vsnprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), fmt, + ap); + va_end(ap); + + (void)fputs(buf, debugfp); + } +} + +# define json_debug_trace(args) (void) json_trace args +#else +# define json_debug_trace(args) /*@i1@*/do { } while (0) +#endif /* DEBUG_ENABLE */ + +/* + * Berkeley implementation of strtod(), inlined to avoid locale problems + * with the decimal point and stripped down to an atof()-equivalent. + */ + +/*@-shiftimplementation +charint@*/ +static double safe_atof(const char *string) +/* Takes a decimal ASCII floating-point number, optionally + * preceded by white space. Must have form "-I.FE-X", + * where I is the integer part of the mantissa, F is + * the fractional part of the mantissa, and X is the + * exponent. Either of the signs may be "+", "-", or + * omitted. Either I or F may be omitted, or both. + * The decimal point isn't necessary unless F is + * present. The "E" may actually be an "e". E and X + * may both be omitted (but not just one). + */ +{ + static int maxExponent = 511; /* Largest possible base 10 exponent. Any + * exponent larger than this will already + * produce underflow or overflow, so there's + * no need to worry about additional digits. + */ + static double powersOf10[] = { /* Table giving binary powers of 10. Entry */ + 10., /* is 10^2^i. Used to convert decimal */ + 100., /* exponents into floating-point numbers. */ + 1.0e4, + 1.0e8, + 1.0e16, + 1.0e32, + 1.0e64, + 1.0e128, + 1.0e256 + }; + + bool sign, expSign = false; + double fraction, dblExp, *d; + register const char *p; + register int c; + int exp = 0; /* Exponent read from "EX" field. */ + int fracExp = 0; /* Exponent that derives from the fractional + * part. Under normal circumstatnces, it is + * the negative of the number of digits in F. + * However, if I is very long, the last digits + * of I get dropped (otherwise a long I with a + * large negative exponent could cause an + * unnecessary overflow on I alone). In this + * case, fracExp is incremented one for each + * dropped digit. */ + int mantSize; /* Number of digits in mantissa. */ + int decPt; /* Number of mantissa digits BEFORE decimal + * point. */ + const char *pExp; /* Temporarily holds location of exponent + * in string. */ + + /* + * Strip off leading blanks and check for a sign. + */ + + p = string; + while (isspace((unsigned char) *p)) { + p += 1; + } + if (*p == '-') { + sign = true; + p += 1; + } else { + if (*p == '+') { + p += 1; + } + sign = false; + } + + /* + * Count the number of digits in the mantissa (including the decimal + * point), and also locate the decimal point. + */ + + decPt = -1; + for (mantSize = 0; ; mantSize += 1) + { + c = *p; + if (!isdigit(c)) { + if ((c != '.') || (decPt >= 0)) { + break; + } + decPt = mantSize; + } + p += 1; + } + + /* + * Now suck up the digits in the mantissa. Use two integers to + * collect 9 digits each (this is faster than using floating-point). + * If the mantissa has more than 18 digits, ignore the extras, since + * they can't affect the value anyway. + */ + + pExp = p; + p -= mantSize; + if (decPt < 0) { + decPt = mantSize; + } else { + mantSize -= 1; /* One of the digits was the point. */ + } + if (mantSize > 18) { + fracExp = decPt - 18; + mantSize = 18; + } else { + fracExp = decPt - mantSize; + } + if (mantSize == 0) { + fraction = 0.0; + //p = string; + goto done; + } else { + int frac1, frac2; + frac1 = 0; + for ( ; mantSize > 9; mantSize -= 1) + { + c = *p; + p += 1; + if (c == '.') { + c = *p; + p += 1; + } + frac1 = 10*frac1 + (c - '0'); + } + frac2 = 0; + for (; mantSize > 0; mantSize -= 1) + { + c = *p; + p += 1; + if (c == '.') { + c = *p; + p += 1; + } + frac2 = 10*frac2 + (c - '0'); + } + fraction = (1.0e9 * frac1) + frac2; + } + + /* + * Skim off the exponent. + */ + + p = pExp; + if ((*p == 'E') || (*p == 'e')) { + p += 1; + if (*p == '-') { + expSign = true; + p += 1; + } else { + if (*p == '+') { + p += 1; + } + expSign = false; + } + while (isdigit((unsigned char) *p)) { + exp = exp * 10 + (*p - '0'); + p += 1; + } + } + if (expSign) { + exp = fracExp - exp; + } else { + exp = fracExp + exp; + } + + /* + * Generate a floating-point number that represents the exponent. + * Do this by processing the exponent one bit at a time to combine + * many powers of 2 of 10. Then combine the exponent with the + * fraction. + */ + + if (exp < 0) { + expSign = true; + exp = -exp; + } else { + expSign = false; + } + if (exp > maxExponent) { + exp = maxExponent; + errno = ERANGE; + } + dblExp = 1.0; + for (d = powersOf10; exp != 0; exp >>= 1, d += 1) { + if (exp & 01) { + dblExp *= *d; + } + } + if (expSign) { + fraction /= dblExp; + } else { + fraction *= dblExp; + } + +done: + if (sign) { + return -fraction; + } + return fraction; +} +/*@+shiftimplementation -charint@*/ + +#define MONTHSPERYEAR 12 /* months per calendar year */ + +static time_t mkutctime(register struct tm * t) +/* struct tm to seconds since Unix epoch */ +{ + register int year; + register time_t result; + static const int cumdays[MONTHSPERYEAR] = + { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; + + /*@ +matchanyintegral @*/ + year = 1900 + t->tm_year + t->tm_mon / MONTHSPERYEAR; + result = (year - 1970) * 365 + cumdays[t->tm_mon % MONTHSPERYEAR]; + result += (year - 1968) / 4; + result -= (year - 1900) / 100; + result += (year - 1600) / 400; + if ((year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0) && + (t->tm_mon % MONTHSPERYEAR) < 2) + result--; + result += t->tm_mday - 1; + result *= 24; + result += t->tm_hour; + result *= 60; + result += t->tm_min; + result *= 60; + result += t->tm_sec; + /*@ -matchanyintegral @*/ + return (result); +} + +static double iso8601_to_unix(/*@in@*/const char *isotime) +/* ISO8601 UTC to Unix UTC */ +{ + char *dp = NULL; + double usec; + struct tm tm; + + /*@i1@*/ dp = strptime(isotime, "%Y-%m-%dT%H:%M:%S", &tm); + if (dp != NULL && *dp == '.') + usec = strtod(dp, NULL); + else + usec = 0; + return mkutctime(&tm) + usec; +} + +/*@-immediatetrans -dependenttrans -usereleased -compdef@*/ +static /*@null@*/ char *json_target_address(const struct json_attr_t *cursor, + /*@null@*/ + const struct json_array_t + *parent, int offset) +{ + char *targetaddr = NULL; + if (parent == NULL || parent->element_type != t_structobject) { + /* ordinary case - use the address in the cursor structure */ + switch (cursor->type) { + case t_ignore: + targetaddr = NULL; + break; + case t_integer: + targetaddr = (char *)&cursor->addr.integer[offset]; + break; + case t_uinteger: + targetaddr = (char *)&cursor->addr.uinteger[offset]; + break; + case t_time: + case t_real: + targetaddr = (char *)&cursor->addr.real[offset]; + break; + case t_string: + targetaddr = cursor->addr.string; + break; + case t_boolean: + targetaddr = (char *)&cursor->addr.boolean[offset]; + break; + case t_character: + targetaddr = (char *)&cursor->addr.character[offset]; + break; + default: + targetaddr = NULL; + break; + } + } else + /* tricky case - hacking a member in an array of structures */ + targetaddr = + parent->arr.objects.base + (offset * parent->arr.objects.stride) + + cursor->addr.offset; + json_debug_trace((1, "Target address for %s (offset %d) is %p\n", + cursor->attribute, offset, targetaddr)); + return targetaddr; +} + +/*@-immediatetrans -dependenttrans +usereleased +compdef@*/ + +static int json_internal_read_object(const char *cp, + const struct json_attr_t *attrs, + /*@null@*/ + const struct json_array_t *parent, + int offset, + /*@null@*/ const char **end) +{ + /*@ -nullstate -nullderef -mustfreefresh -nullpass -usedef @*/ + enum + { init, await_attr, in_attr, await_value, in_val_string, + in_escape, in_val_token, post_val, post_array + } state = 0; +#ifdef DEBUG_ENABLE + char *statenames[] = { + "init", "await_attr", "in_attr", "await_value", "in_val_string", + "in_escape", "in_val_token", "post_val", "post_array", + }; +#endif /* DEBUG_ENABLE */ + char attrbuf[JSON_ATTR_MAX + 1], *pattr = NULL; + char valbuf[JSON_VAL_MAX + 1], *pval = NULL; + bool value_quoted = false; + char uescape[5]; /* enough space for 4 hex digits and a NUL */ + const struct json_attr_t *cursor; + int substatus, n, maxlen = 0; + unsigned int u; + const struct json_enum_t *mp; + char *lptr; + +#ifdef S_SPLINT_S + /* prevents gripes about buffers not being completely defined */ + memset(valbuf, '\0', sizeof(valbuf)); + memset(attrbuf, '\0', sizeof(attrbuf)); +#endif /* S_SPLINT_S */ + + if (end != NULL) + *end = NULL; /* give it a well-defined value on parse failure */ + + /* stuff fields with defaults in case they're omitted in the JSON input */ + for (cursor = attrs; cursor->attribute != NULL; cursor++) + if (!cursor->nodefault) { + lptr = json_target_address(cursor, parent, offset); + if (lptr != NULL) + switch (cursor->type) { + case t_integer: + memcpy(lptr, &cursor->dflt.integer, sizeof(int)); + break; + case t_uinteger: + memcpy(lptr, &cursor->dflt.uinteger, sizeof(unsigned int)); + break; + case t_time: + case t_real: + memcpy(lptr, &cursor->dflt.real, sizeof(double)); + break; + case t_string: + if (parent != NULL + && parent->element_type != t_structobject + && offset > 0) + return JSON_ERR_NOPARSTR; + lptr[0] = '\0'; + break; + case t_boolean: + memcpy(lptr, &cursor->dflt.boolean, sizeof(bool)); + break; + case t_character: + lptr[0] = cursor->dflt.character; + break; + case t_object: /* silences a compiler warning */ + case t_structobject: + case t_array: + case t_check: + case t_ignore: + break; + } + } + + json_debug_trace((1, "JSON parse of '%s' begins.\n", cp)); + + /* parse input JSON */ + for (; *cp != '\0'; cp++) { + json_debug_trace((2, "State %-14s, looking at '%c' (%p)\n", + statenames[state], *cp, cp)); + switch (state) { + case init: + if (isspace((unsigned char) *cp)) + continue; + else if (*cp == '{') + state = await_attr; + else { + json_debug_trace((1, + "Non-WS when expecting object start.\n")); + return JSON_ERR_OBSTART; + } + break; + case await_attr: + if (isspace((unsigned char) *cp)) + continue; + else if (*cp == '"') { + state = in_attr; + pattr = attrbuf; + } else if (*cp == '}') + break; + else { + json_debug_trace((1, "Non-WS when expecting attribute.\n")); + return JSON_ERR_ATTRSTART; + } + break; + case in_attr: + if (pattr == NULL) + return JSON_ERR_NULLPTR; + if (*cp == '"') { + *pattr++ = '\0'; + json_debug_trace((1, "Collected attribute name %s\n", + attrbuf)); + for (cursor = attrs; cursor->attribute != NULL; cursor++) { + json_debug_trace((2, "Checking against %s\n", + cursor->attribute)); + if (strcmp(cursor->attribute, attrbuf) == 0) + break; + } + if (cursor->attribute == NULL) { + json_debug_trace((1, + "Unknown attribute name '%s' (attributes begin with '%s').\n", + attrbuf, attrs->attribute)); + return JSON_ERR_BADATTR; + } + state = await_value; + if (cursor->type == t_string) + maxlen = (int)cursor->len - 1; + else if (cursor->type == t_check) + maxlen = (int)strlen(cursor->dflt.check); + else if (cursor->type == t_time || cursor->type == t_ignore) + maxlen = JSON_VAL_MAX; + else if (cursor->map != NULL) + maxlen = (int)sizeof(valbuf) - 1; + pval = valbuf; + } else if (pattr >= attrbuf + JSON_ATTR_MAX - 1) { + json_debug_trace((1, "Attribute name too long.\n")); + return JSON_ERR_ATTRLEN; + } else + *pattr++ = *cp; + break; + case await_value: + if (isspace((unsigned char) *cp) || *cp == ':') + continue; + else if (*cp == '[') { + if (cursor->type != t_array) { + json_debug_trace((1, + "Saw [ when not expecting array.\n")); + return JSON_ERR_NOARRAY; + } + substatus = json_read_array(cp, &cursor->addr.array, &cp); + if (substatus != 0) + return substatus; + state = post_array; + } else if (cursor->type == t_array) { + json_debug_trace((1, + "Array element was specified, but no [.\n")); + return JSON_ERR_NOBRAK; + } else if (*cp == '"') { + value_quoted = true; + state = in_val_string; + pval = valbuf; + } else { + value_quoted = false; + state = in_val_token; + pval = valbuf; + *pval++ = *cp; + } + break; + case in_val_string: + if (pval == NULL) + return JSON_ERR_NULLPTR; + if (*cp == '\\') + state = in_escape; + else if (*cp == '"') { + *pval++ = '\0'; + json_debug_trace((1, "Collected string value %s\n", valbuf)); + state = post_val; + } else if (pval > valbuf + JSON_VAL_MAX - 1 + || pval > valbuf + maxlen) { + json_debug_trace((1, "String value too long.\n")); + return JSON_ERR_STRLONG; /* */ + } else + *pval++ = *cp; + break; + case in_escape: + if (pval == NULL) + return JSON_ERR_NULLPTR; + switch (*cp) { + case 'b': + *pval++ = '\b'; + break; + case 'f': + *pval++ = '\f'; + break; + case 'n': + *pval++ = '\n'; + break; + case 'r': + *pval++ = '\r'; + break; + case 't': + *pval++ = '\t'; + break; + case 'u': + for (n = 0; n < 4 && cp[n] != '\0'; n++) + uescape[n] = *cp++; + --cp; + (void)sscanf(uescape, "%04x", &u); + *pval++ = (char)u; /* will truncate values above 0xff */ + break; + default: /* handles double quote and solidus */ + *pval++ = *cp; + break; + } + state = in_val_string; + break; + case in_val_token: + if (pval == NULL) + return JSON_ERR_NULLPTR; + if (isspace((unsigned char) *cp) || *cp == ',' || *cp == '}') { + *pval = '\0'; + json_debug_trace((1, "Collected token value %s.\n", valbuf)); + state = post_val; + if (*cp == '}' || *cp == ',') + --cp; + } else if (pval > valbuf + JSON_VAL_MAX - 1) { + json_debug_trace((1, "Token value too long.\n")); + return JSON_ERR_TOKLONG; + } else + *pval++ = *cp; + break; + case post_val: + /* + * We know that cursor points at the first spec matching + * the current attribute. We don't know that it's *the* + * correct spec; our dialect allows there to be any number + * of adjacent ones with the same attrname but different + * types. Here's where we try to seek forward for a + * matching type/attr pair if we're not looking at one. + */ + for (;;) { + int seeking = cursor->type; + if (value_quoted && (cursor->type == t_string || cursor->type == t_time)) + break; + if ((strcmp(valbuf, "true")==0 || strcmp(valbuf, "false")==0) + && seeking == t_boolean) + break; + if (isdigit((unsigned char) valbuf[0])) { + bool decimal = strchr(valbuf, '.') != NULL; + if (decimal && seeking == t_real) + break; + if (!decimal && (seeking == t_integer || seeking == t_uinteger)) + break; + } + if (cursor[1].attribute==NULL) /* out of possiblities */ + break; + if (strcmp(cursor[1].attribute, attrbuf)!=0) + break; + ++cursor; + } + if (value_quoted + && (cursor->type != t_string && cursor->type != t_character + && cursor->type != t_check && cursor->type != t_time + && cursor->type != t_ignore && cursor->map == 0)) { + json_debug_trace((1, + "Saw quoted value when expecting non-string.\n")); + return JSON_ERR_QNONSTRING; + } + if (!value_quoted + && (cursor->type == t_string || cursor->type == t_check + || cursor->type == t_time || cursor->map != 0)) { + json_debug_trace((1, + "Didn't see quoted value when expecting string.\n")); + return JSON_ERR_NONQSTRING; + } + if (cursor->map != 0) { + for (mp = cursor->map; mp->name != NULL; mp++) + if (strcmp(mp->name, valbuf) == 0) { + goto foundit; + } + json_debug_trace((1, "Invalid enumerated value string %s.\n", + valbuf)); + return JSON_ERR_BADENUM; + foundit: + (void)snprintf(valbuf, sizeof(valbuf), "%d", mp->value); + } + lptr = json_target_address(cursor, parent, offset); + if (lptr != NULL) + switch (cursor->type) { + case t_integer: + { + int tmp = atoi(valbuf); + memcpy(lptr, &tmp, sizeof(int)); + } + break; + case t_uinteger: + { + unsigned int tmp = (unsigned int)atoi(valbuf); + memcpy(lptr, &tmp, sizeof(unsigned int)); + } + break; + case t_time: + { + double tmp = iso8601_to_unix(valbuf); + memcpy(lptr, &tmp, sizeof(double)); + } + break; + case t_real: + { + double tmp = safe_atof(valbuf); + memcpy(lptr, &tmp, sizeof(double)); + } + break; + case t_string: + if (parent != NULL + && parent->element_type != t_structobject + && offset > 0) + return JSON_ERR_NOPARSTR; + (void)strncpy(lptr, valbuf, cursor->len); + valbuf[sizeof(valbuf)-1] = '\0'; + break; + case t_boolean: + { + bool tmp = (strcmp(valbuf, "true") == 0); + memcpy(lptr, &tmp, sizeof(bool)); + } + break; + case t_character: + if (strlen(valbuf) > 1) + return JSON_ERR_STRLONG; + else + lptr[0] = valbuf[0]; + break; + case t_ignore: /* silences a compiler warning */ + case t_object: /* silences a compiler warning */ + case t_structobject: + case t_array: + break; + case t_check: + if (strcmp(cursor->dflt.check, valbuf) != 0) { + json_debug_trace((1, + "Required attribute vakue %s not present.\n", + cursor->dflt.check)); + return JSON_ERR_CHECKFAIL; + } + break; + } + /*@fallthrough@*/ + case post_array: + if (isspace((unsigned char) *cp)) + continue; + else if (*cp == ',') + state = await_attr; + else if (*cp == '}') { + ++cp; + goto good_parse; + } else { + json_debug_trace((1, "Garbage while expecting comma or }\n")); + return JSON_ERR_BADTRAIL; + } + break; + } + } + + good_parse: + /* in case there's another object following, consune trailing WS */ + while (isspace((unsigned char) *cp)) + ++cp; + if (end != NULL) + *end = cp; + json_debug_trace((1, "JSON parse ends.\n")); + return 0; + /*@ +nullstate +nullderef +mustfreefresh +nullpass +usedef @*/ +} + +int json_read_array(const char *cp, const struct json_array_t *arr, + const char **end) +{ + /*@-nullstate -onlytrans@*/ + int substatus, offset, arrcount; + char *tp; + + if (end != NULL) + *end = NULL; /* give it a well-defined value on parse failure */ + + json_debug_trace((1, "Entered json_read_array()\n")); + + while (isspace((unsigned char) *cp)) + cp++; + if (*cp != '[') { + json_debug_trace((1, "Didn't find expected array start\n")); + return JSON_ERR_ARRAYSTART; + } else + cp++; + + tp = arr->arr.strings.store; + arrcount = 0; + + /* Check for empty array */ + while (isspace((unsigned char) *cp)) + cp++; + if (*cp == ']') + goto breakout; + + for (offset = 0; offset < arr->maxlen; offset++) { + json_debug_trace((1, "Looking at %s\n", cp)); + switch (arr->element_type) { + case t_string: + if (isspace((unsigned char) *cp)) + cp++; + if (*cp != '"') + return JSON_ERR_BADSTRING; + else + ++cp; + arr->arr.strings.ptrs[offset] = tp; + for (; tp - arr->arr.strings.store < arr->arr.strings.storelen; + tp++) + if (*cp == '"') { + ++cp; + *tp++ = '\0'; + goto stringend; + } else if (*cp == '\0') { + json_debug_trace((1, + "Bad string syntax in string list.\n")); + return JSON_ERR_BADSTRING; + } else { + *tp = *cp++; + } + json_debug_trace((1, "Bad string syntax in string list.\n")); + return JSON_ERR_BADSTRING; + stringend: + break; + case t_object: + case t_structobject: + substatus = + json_internal_read_object(cp, arr->arr.objects.subtype, arr, + offset, &cp); + if (substatus != 0) + return substatus; + break; + case t_integer: + case t_uinteger: + case t_time: + case t_real: + case t_boolean: + case t_character: + case t_array: + case t_check: + case t_ignore: + json_debug_trace((1, "Invalid array subtype.\n")); + return JSON_ERR_SUBTYPE; + } + arrcount++; + if (isspace((unsigned char) *cp)) + cp++; + if (*cp == ']') { + json_debug_trace((1, "End of array found.\n")); + goto breakout; + } else if (*cp == ',') + cp++; + else { + json_debug_trace((1, "Bad trailing syntax on array.\n")); + return JSON_ERR_BADSUBTRAIL; + } + } + json_debug_trace((1, "Too many elements in array.\n")); + return JSON_ERR_SUBTOOLONG; + breakout: + if (arr->count != NULL) + *(arr->count) = arrcount; + if (end != NULL) + *end = cp; + /*@ -nullderef @*/ + json_debug_trace((1, "leaving json_read_array() with %d elements\n", + arrcount)); + /*@ +nullderef @*/ + return 0; + /*@+nullstate +onlytrans@*/ +} + +int json_read_object(const char *cp, const struct json_attr_t *attrs, + /*@null@*/ const char **end) +{ + int st; + + json_debug_trace((1, "json_read_object() sees '%s'\n", cp)); + st = json_internal_read_object(cp, attrs, NULL, 0, end); + return st; +} + +const /*@observer@*/ char *json_error_string(int err) +{ + const char *errors[] = { + "unknown error while parsing JSON", + "non-whitespace when expecting object start", + "non-whitespace when expecting attribute start", + "unknown attribute name", + "attribute name too long", + "saw [ when not expecting array", + "array element specified, but no [", + "string value too long", + "token value too long", + "garbage while expecting , or }", + "didn't find expected array start", + "error while parsing object array", + "too many array elements", + "garbage while expecting array comma", + "unsupported array element type", + "error while string parsing", + "check attribute not matched", + "can't support strings in parallel arrays", + "invalid enumerated value", + "saw quoted value when expecting nonstring", + "didn't see quoted value when expecting string", + "other data conversion error", + "unexpected null value or attribute pointer", + }; + + if (err <= 0 || err >= (int)(sizeof(errors) / sizeof(errors[0]))) + return errors[0]; + else + return errors[err]; +} + +/* end */ + diff --git a/microjson.h b/microjson.h new file mode 100644 index 0000000..8f60917 --- /dev/null +++ b/microjson.h @@ -0,0 +1,125 @@ +/* Structures for JSON parsing using only fixed-extent memory + * + * This file is Copyright (c) 2010 by the GPSD project + * BSD terms apply: see the file COPYING in the distribution root for details. + */ + +#include +#include + +typedef enum {t_integer, t_uinteger, t_real, + t_string, t_boolean, t_character, + t_time, + t_object, t_structobject, t_array, + t_check, t_ignore} + json_type; + +struct json_enum_t { + char *name; + int value; +}; + +struct json_array_t { + json_type element_type; + union { + struct { + const struct json_attr_t *subtype; + char *base; + size_t stride; + } objects; + struct { + char **ptrs; + char *store; + int storelen; + } strings; + } arr; + int *count, maxlen; +}; + +struct json_attr_t { + char *attribute; + json_type type; + union { + int *integer; + unsigned int *uinteger; + double *real; + char *string; + bool *boolean; + char *character; + struct json_array_t array; + size_t offset; + } addr; + union { + int integer; + unsigned int uinteger; + double real; + bool boolean; + char character; + char *check; + } dflt; + size_t len; + const struct json_enum_t *map; + bool nodefault; +}; + +#define JSON_ATTR_MAX 31 /* max chars in JSON attribute name */ +#define JSON_VAL_MAX 512 /* max chars in JSON value part */ + +#ifdef __cplusplus +extern "C" { +#endif +int json_read_object(const char *, const struct json_attr_t *, + /*@null@*/const char **); +int json_read_array(const char *, const struct json_array_t *, + /*@null@*/const char **); +const /*@observer@*/char *json_error_string(int); + +void json_enable_debug(int, FILE *); +#ifdef __cplusplus +} +#endif + +#define JSON_ERR_OBSTART 1 /* non-WS when expecting object start */ +#define JSON_ERR_ATTRSTART 2 /* non-WS when expecting attrib start */ +#define JSON_ERR_BADATTR 3 /* unknown attribute name */ +#define JSON_ERR_ATTRLEN 4 /* attribute name too long */ +#define JSON_ERR_NOARRAY 5 /* saw [ when not expecting array */ +#define JSON_ERR_NOBRAK 6 /* array element specified, but no [ */ +#define JSON_ERR_STRLONG 7 /* string value too long */ +#define JSON_ERR_TOKLONG 8 /* token value too long */ +#define JSON_ERR_BADTRAIL 9 /* garbage while expecting , or } */ +#define JSON_ERR_ARRAYSTART 10 /* didn't find expected array start */ +#define JSON_ERR_OBJARR 11 /* error while parsing object array */ +#define JSON_ERR_SUBTOOLONG 12 /* too many array elements */ +#define JSON_ERR_BADSUBTRAIL 13 /* garbage while expecting array comma */ +#define JSON_ERR_SUBTYPE 14 /* unsupported array element type */ +#define JSON_ERR_BADSTRING 15 /* error while string parsing */ +#define JSON_ERR_CHECKFAIL 16 /* check attribute not matched */ +#define JSON_ERR_NOPARSTR 17 /* can't support strings in parallel arrays */ +#define JSON_ERR_BADENUM 18 /* invalid enumerated value */ +#define JSON_ERR_QNONSTRING 19 /* saw quoted value when expecting nonstring */ +#define JSON_ERR_NONQSTRING 19 /* didn't see quoted value when expecting string */ +#define JSON_ERR_MISC 20 /* other data conversion error */ +#define JSON_ERR_BADNUM 21 /* error while parsing a numerical argument */ +#define JSON_ERR_NULLPTR 22 /* unexpected null value or attribute pointer */ + +/* + * Use the following macros to declare template initializers for structobject + * arrays. Writing the equivalents out by hand is error-prone. + * + * STRUCTOBJECT takes a structure name s, and a fieldname f in s. + * + * STRUCTARRAY takes the name of a structure array, a pointer to a an + * initializer defining the subobject type, and the address of an integer to + * store the length in. + */ +#define STRUCTOBJECT(s, f) .addr.offset = offsetof(s, f) +#define STRUCTARRAY(a, e, n) \ + .addr.array.element_type = t_structobject, \ + .addr.array.arr.objects.subtype = e, \ + .addr.array.arr.objects.base = (char*)a, \ + .addr.array.arr.objects.stride = sizeof(a[0]), \ + .addr.array.count = n, \ + .addr.array.maxlen = NITEMS(a) + +/* json.h ends here */ diff --git a/test_microjson.c b/test_microjson.c new file mode 100644 index 0000000..d7b99e0 --- /dev/null +++ b/test_microjson.c @@ -0,0 +1,751 @@ +/* test_microjson.c - unit test for JSON parsing into fixed-extent structures + * + * This file is Copyright (c) 2010 by the GPSD project + * BSD terms apply: see the file COPYING in the distribution root for details. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "microjson.h" + +#define MAXCHANNELS 20 +#define MAXUSERDEVS 4 +#define JSON_DATE_MAX 24 /* ISO8601 timestamp with 2 decimal places */ + +/* these values don't matter in themselves, they just have to be out-of-band */ +#define DEVDEFAULT_BPS 0 +#define DEVDEFAULT_PARITY 'X' +#define DEVDEFAULT_STOPBITS 3 +#define DEVDEFAULT_NATIVE -1 + +#define NITEMS(x) (int)(sizeof(x)/sizeof(x[0])) + +typedef double timestamp_t; /* Unix time in seconds with fractional part */ + +struct dop_t { + /* Dilution of precision factors */ + double xdop, ydop, pdop, hdop, vdop, tdop, gdop; +}; + +struct version_t { + char release[64]; /* external version */ + char rev[64]; /* internal revision ID */ + int proto_major, proto_minor; /* API major and minor versions */ + char remote[PATH_MAX]; /* could be from a remote device */ +}; + +struct devconfig_t { + char path[PATH_MAX]; + int flags; +#define SEEN_GPS 0x01 +#define SEEN_RTCM2 0x02 +#define SEEN_RTCM3 0x04 +#define SEEN_AIS 0x08 + char driver[64]; + char subtype[64]; + double activated; + unsigned int baudrate, stopbits; /* RS232 link parameters */ + char parity; /* 'N', 'O', or 'E' */ + double cycle, mincycle; /* refresh cycle time in seconds */ + int driver_mode; /* is driver in native mode or not? */ +}; +struct gps_fix_t { + timestamp_t time; /* Time of update */ + int mode; /* Mode of fix */ +#define MODE_NOT_SEEN 0 /* mode update not seen yet */ +#define MODE_NO_FIX 1 /* none */ +#define MODE_2D 2 /* good for latitude/longitude */ +#define MODE_3D 3 /* good for altitude/climb too */ + double ept; /* Expected time uncertainty */ + double latitude; /* Latitude in degrees (valid if mode >= 2) */ + double epy; /* Latitude position uncertainty, meters */ + double longitude; /* Longitude in degrees (valid if mode >= 2) */ + double epx; /* Longitude position uncertainty, meters */ + double altitude; /* Altitude in meters (valid if mode == 3) */ + double epv; /* Vertical position uncertainty, meters */ + double track; /* Course made good (relative to true north) */ + double epd; /* Track uncertainty, degrees */ + double speed; /* Speed over ground, meters/sec */ + double eps; /* Speed uncertainty, meters/sec */ + double climb; /* Vertical speed, meters/sec */ + double epc; /* Vertical speed uncertainty */ +}; + +struct gps_data_t { + struct gps_fix_t fix; /* accumulated PVT data */ + + /* this should move to the per-driver structure */ + double separation; /* Geoidal separation, MSL - WGS84 (Meters) */ + + /* GPS status -- always valid */ + int status; /* Do we have a fix? */ +#define STATUS_NO_FIX 0 /* no */ +#define STATUS_FIX 1 /* yes, without DGPS */ +#define STATUS_DGPS_FIX 2 /* yes, with DGPS */ + + /* precision of fix -- valid if satellites_used > 0 */ + int satellites_used; /* Number of satellites used in solution */ + int used[MAXCHANNELS]; /* PRNs of satellites used in solution */ + struct dop_t dop; + + /* redundant with the estimate elements in the fix structure */ + double epe; /* spherical position error, 95% confidence (meters) */ + + /* satellite status -- valid when satellites_visible > 0 */ + timestamp_t skyview_time; /* skyview timestamp */ + int satellites_visible; /* # of satellites in view */ + int PRN[MAXCHANNELS]; /* PRNs of satellite */ + int elevation[MAXCHANNELS]; /* elevation of satellite */ + int azimuth[MAXCHANNELS]; /* azimuth */ + double ss[MAXCHANNELS]; /* signal-to-noise ratio (dB) */ + + struct devconfig_t dev; /* device that shipped last update */ + + struct { + timestamp_t time; + int ndevices; + struct devconfig_t list[MAXUSERDEVS]; + } devices; + + struct version_t version; +}; + +static struct gps_data_t gpsdata; + +/* + * There's a splint limitation that parameters can be declared + * @out@ or @null@ but not, apparently, both. This collides with + * the (admittedly tricky) way we use endptr. The workaround is to + * declare it @null@ and use -compdef around the JSON reader calls. + */ +/*@-compdef@*/ + +static int json_tpv_read(const char *buf, struct gps_data_t *gpsdata, + /*@null@*/ const char **endptr) +{ + /*@ -fullinitblock @*/ + const struct json_attr_t json_attrs_1[] = { + /* *INDENT-OFF* */ + {"class", t_check, .dflt.check = "TPV"}, + {"device", t_string, .addr.string = gpsdata->dev.path, + .len = sizeof(gpsdata->dev.path)}, + {"time", t_time, .addr.real = &gpsdata->fix.time, + .dflt.real = NAN}, + {"time", t_real, .addr.real = &gpsdata->fix.time, + .dflt.real = NAN}, + {"ept", t_real, .addr.real = &gpsdata->fix.ept, + .dflt.real = NAN}, + {"lon", t_real, .addr.real = &gpsdata->fix.longitude, + .dflt.real = NAN}, + {"lat", t_real, .addr.real = &gpsdata->fix.latitude, + .dflt.real = NAN}, + {"alt", t_real, .addr.real = &gpsdata->fix.altitude, + .dflt.real = NAN}, + {"epx", t_real, .addr.real = &gpsdata->fix.epx, + .dflt.real = NAN}, + {"epy", t_real, .addr.real = &gpsdata->fix.epy, + .dflt.real = NAN}, + {"epv", t_real, .addr.real = &gpsdata->fix.epv, + .dflt.real = NAN}, + {"track", t_real, .addr.real = &gpsdata->fix.track, + .dflt.real = NAN}, + {"speed", t_real, .addr.real = &gpsdata->fix.speed, + .dflt.real = NAN}, + {"climb", t_real, .addr.real = &gpsdata->fix.climb, + .dflt.real = NAN}, + {"epd", t_real, .addr.real = &gpsdata->fix.epd, + .dflt.real = NAN}, + {"eps", t_real, .addr.real = &gpsdata->fix.eps, + .dflt.real = NAN}, + {"epc", t_real, .addr.real = &gpsdata->fix.epc, + .dflt.real = NAN}, + {"mode", t_integer, .addr.integer = &gpsdata->fix.mode, + .dflt.integer = MODE_NOT_SEEN}, + {NULL}, + /* *INDENT-ON* */ + }; + /*@ +fullinitblock @*/ + + return json_read_object(buf, json_attrs_1, endptr); +} + +static int json_sky_read(const char *buf, struct gps_data_t *gpsdata, + /*@null@*/ const char **endptr) +{ + bool usedflags[MAXCHANNELS]; + /*@ -fullinitblock @*/ + const struct json_attr_t json_attrs_2_1[] = { + /* *INDENT-OFF* */ + {"PRN", t_integer, .addr.integer = gpsdata->PRN}, + {"el", t_integer, .addr.integer = gpsdata->elevation}, + {"az", t_integer, .addr.integer = gpsdata->azimuth}, + {"ss", t_real, .addr.real = gpsdata->ss}, + {"used", t_boolean, .addr.boolean = usedflags}, + /* *INDENT-ON* */ + {NULL}, + }; + const struct json_attr_t json_attrs_2[] = { + /* *INDENT-OFF* */ + {"class", t_check, .dflt.check = "SKY"}, + {"device", t_string, .addr.string = gpsdata->dev.path, + .len = sizeof(gpsdata->dev.path)}, + {"time", t_time, .addr.real = &gpsdata->skyview_time, + .dflt.real = NAN}, + {"time", t_real, .addr.real = &gpsdata->skyview_time, + .dflt.real = NAN}, + {"hdop", t_real, .addr.real = &gpsdata->dop.hdop, + .dflt.real = NAN}, + {"xdop", t_real, .addr.real = &gpsdata->dop.xdop, + .dflt.real = NAN}, + {"ydop", t_real, .addr.real = &gpsdata->dop.ydop, + .dflt.real = NAN}, + {"vdop", t_real, .addr.real = &gpsdata->dop.vdop, + .dflt.real = NAN}, + {"tdop", t_real, .addr.real = &gpsdata->dop.tdop, + .dflt.real = NAN}, + {"pdop", t_real, .addr.real = &gpsdata->dop.pdop, + .dflt.real = NAN}, + {"gdop", t_real, .addr.real = &gpsdata->dop.gdop, + .dflt.real = NAN}, + {"satellites", t_array, .addr.array.element_type = t_object, + .addr.array.arr.objects.subtype=json_attrs_2_1, + .addr.array.maxlen = MAXCHANNELS, + .addr.array.count = &gpsdata->satellites_visible}, + {NULL}, + /* *INDENT-ON* */ + }; + /*@ +fullinitblock @*/ + int status, i, j; + + for (i = 0; i < MAXCHANNELS; i++) { + gpsdata->PRN[i] = 0; + usedflags[i] = false; + } + + status = json_read_object(buf, json_attrs_2, endptr); + if (status != 0) + return status; + + gpsdata->satellites_used = 0; + gpsdata->satellites_visible = 0; + (void)memset(gpsdata->used, '\0', sizeof(gpsdata->used)); + for (i = j = 0; i < MAXCHANNELS; i++) { + if(gpsdata->PRN[i] > 0) + gpsdata->satellites_visible++; + if (usedflags[i]) { + gpsdata->used[j++] = gpsdata->PRN[i]; + gpsdata->satellites_used++; + } + } + + return 0; +} + +static int json_devicelist_read(const char *buf, struct gps_data_t *gpsdata, + /*@null@*/ const char **endptr) +{ + /*@ -fullinitblock @*/ + const struct json_attr_t json_attrs_subdevices[] = { + /* *INDENT-OFF* */ + {"class", t_check, .dflt.check = "DEVICE"}, + {"path", t_string, STRUCTOBJECT(struct devconfig_t, path), + .len = sizeof(gpsdata->devices.list[0].path)}, + {"activated", t_time, STRUCTOBJECT(struct devconfig_t, activated)}, + {"activated", t_real, STRUCTOBJECT(struct devconfig_t, activated)}, + {"flags", t_integer, STRUCTOBJECT(struct devconfig_t, flags)}, + {"driver", t_string, STRUCTOBJECT(struct devconfig_t, driver), + .len = sizeof(gpsdata->devices.list[0].driver)}, + {"subtype", t_string, STRUCTOBJECT(struct devconfig_t, subtype), + .len = sizeof(gpsdata->devices.list[0].subtype)}, + {"native", t_integer, STRUCTOBJECT(struct devconfig_t, driver_mode), + .dflt.integer = -1}, + {"bps", t_uinteger, STRUCTOBJECT(struct devconfig_t, baudrate), + .dflt.uinteger = DEVDEFAULT_BPS}, + {"parity", t_character, STRUCTOBJECT(struct devconfig_t, parity), + .dflt.character = DEVDEFAULT_PARITY}, + {"stopbits", t_uinteger, STRUCTOBJECT(struct devconfig_t, stopbits), + .dflt.integer = DEVDEFAULT_STOPBITS}, + {"cycle", t_real, STRUCTOBJECT(struct devconfig_t, cycle), + .dflt.real = NAN}, + {"mincycle", t_real, STRUCTOBJECT(struct devconfig_t, mincycle), + .dflt.real = NAN}, + {NULL}, + /* *INDENT-ON* */ + }; + /*@-type@*//* STRUCTARRAY confuses splint */ + const struct json_attr_t json_attrs_devices[] = { + {"class", t_check,.dflt.check = "DEVICES"}, + {"devices", t_array, STRUCTARRAY(gpsdata->devices.list, + json_attrs_subdevices, + &gpsdata->devices.ndevices)}, + {NULL}, + }; + /*@+type@*/ + /*@ +fullinitblock @*/ + int status; + + memset(&gpsdata->devices, '\0', sizeof(gpsdata->devices)); + status = json_read_object(buf, json_attrs_devices, endptr); + if (status != 0) { + return status; + } + return 0; +} + +static int json_device_read(const char *buf, + /*@out@*/ struct devconfig_t *dev, + /*@null@*/ const char **endptr) +{ + char tbuf[JSON_DATE_MAX+1]; + /*@ -fullinitblock @*/ + /* *INDENT-OFF* */ + const struct json_attr_t json_attrs_device[] = { + {"class", t_check, .dflt.check = "DEVICE"}, + + {"path", t_string, .addr.string = dev->path, + .len = sizeof(dev->path)}, + {"activated", t_string, .addr.string = tbuf, + .len = sizeof(tbuf)}, + {"activated", t_real, .addr.real = &dev->activated}, + {"flags", t_integer, .addr.integer = &dev->flags}, + {"driver", t_string, .addr.string = dev->driver, + .len = sizeof(dev->driver)}, + {"subtype", t_string, .addr.string = dev->subtype, + .len = sizeof(dev->subtype)}, + {"native", t_integer, .addr.integer = &dev->driver_mode, + .dflt.integer = DEVDEFAULT_NATIVE}, + {"bps", t_uinteger, .addr.uinteger = &dev->baudrate, + .dflt.uinteger = DEVDEFAULT_BPS}, + {"parity", t_character, .addr.character = &dev->parity, + .dflt.character = DEVDEFAULT_PARITY}, + {"stopbits", t_uinteger, .addr.uinteger = &dev->stopbits, + .dflt.uinteger = DEVDEFAULT_STOPBITS}, + {"cycle", t_real, .addr.real = &dev->cycle, + .dflt.real = NAN}, + {"mincycle", t_real, .addr.real = &dev->mincycle, + .dflt.real = NAN}, + {NULL}, + }; + /* *INDENT-ON* */ + /*@ +fullinitblock @*/ + int status; + + tbuf[0] = '\0'; + status = json_read_object(buf, json_attrs_device, endptr); + if (status != 0) + return status; + + return 0; +} + +static int json_version_read(const char *buf, struct gps_data_t *gpsdata, + /*@null@*/ const char **endptr) +{ + /*@ -fullinitblock @*/ + const struct json_attr_t json_attrs_version[] = { + /* *INDENT-OFF* */ + {"class", t_check, .dflt.check = "VERSION"}, + {"release", t_string, .addr.string = gpsdata->version.release, + .len = sizeof(gpsdata->version.release)}, + {"rev", t_string, .addr.string = gpsdata->version.rev, + .len = sizeof(gpsdata->version.rev)}, + {"proto_major", t_integer, .addr.integer = &gpsdata->version.proto_major}, + {"proto_minor", t_integer, .addr.integer = &gpsdata->version.proto_minor}, + {"remote", t_string, .addr.string = gpsdata->version.remote, + .len = sizeof(gpsdata->version.remote)}, + {NULL}, + /* *INDENT-ON* */ + }; + /*@ +fullinitblock @*/ + int status; + + memset(&gpsdata->version, '\0', sizeof(gpsdata->version)); + status = json_read_object(buf, json_attrs_version, endptr); + + return status; +} + +static int libgps_json_unpack(const char *buf, + struct gps_data_t *gpsdata, const char **end) +/* the only entry point - unpack a JSON object into gpsdata_t substructures */ +{ + int status; + char *classtag = strstr(buf, "\"class\":"); + + if (classtag == NULL) + return -1; +#define STARTSWITH(str, prefix) strncmp(str, prefix, sizeof(prefix)-1)==0 + if (STARTSWITH(classtag, "\"class\":\"TPV\"")) { + status = json_tpv_read(buf, gpsdata, end); + gpsdata->status = STATUS_FIX; + return status; + } else if (STARTSWITH(classtag, "\"class\":\"SKY\"")) { + status = json_sky_read(buf, gpsdata, end); + return status; + } else if (STARTSWITH(classtag, "\"class\":\"DEVICE\"")) { + status = json_device_read(buf, &gpsdata->dev, end); + return status; + } else if (STARTSWITH(classtag, "\"class\":\"DEVICES\"")) { + status = json_devicelist_read(buf, gpsdata, end); + return status; + } else if (STARTSWITH(classtag, "\"class\":\"VERSION\"")) { + status = json_version_read(buf, gpsdata, end); + return status; + } +#undef STARTSWITH +} + +/*@+compdef@*/ + +static void assert_case(int num, int status) +{ + if (status != 0) { + (void)fprintf(stderr, "case %d FAILED, status %d (%s).\n", num, + status, json_error_string(status)); + exit(EXIT_FAILURE); + } +} + +static void assert_string(char *attr, char *fld, char *val) +{ + if (strcmp(fld, val)) { + (void)fprintf(stderr, + "'%s' string attribute eval failed, value = %s.\n", + attr, fld); + exit(EXIT_FAILURE); + } +} + +static void assert_integer(char *attr, int fld, int val) +{ + if (fld != val) { + (void)fprintf(stderr, + "'%s' integer attribute eval failed, value = %d.\n", + attr, fld); + exit(EXIT_FAILURE); + } +} + +static void assert_uinteger(char *attr, uint fld, uint val) +{ + if (fld != val) { + (void)fprintf(stderr, + "'%s' integer attribute eval failed, value = %u.\n", + attr, fld); + exit(EXIT_FAILURE); + } +} + +static void assert_boolean(char *attr, bool fld, bool val) +{ + /*@-boolcompare@*/ + if (fld != val) { + (void)fprintf(stderr, + "'%s' boolean attribute eval failed, value = %s.\n", + attr, fld ? "true" : "false"); + exit(EXIT_FAILURE); + } + /*@+boolcompare@*/ +} + +/* + * Floating point comparisons are iffy, but at least if any of these fail + * the output will make it clear whether it was a precision issue + */ +static void assert_real(char *attr, double fld, double val) +{ + if (fld != val) { + (void)fprintf(stderr, + "'%s' real attribute eval failed, value = %f.\n", attr, + fld); + exit(EXIT_FAILURE); + } +} + +/*@ -fullinitblock @*/ + +/* Case 1: TPV report */ + +/* *INDENT-OFF* */ +static const char json_str1[] = "{\"class\":\"TPV\",\ + \"device\":\"GPS#1\", \ + \"time\":\"2005-06-19T08:12:41.89Z\",\"lon\":46.498203637,\"lat\":7.568074350,\ + \"alt\":1327.780,\"epx\":21.000,\"epy\":23.000,\"epv\":124.484,\"mode\":3}"; + +/* Case 2: SKY report */ + +static const char *json_str2 = "{\"class\":\"SKY\",\ + \"time\":\"2005-06-19T12:12:42.03Z\", \ + \"satellites\":[\ + {\"PRN\":10,\"el\":45,\"az\":196,\"ss\":34,\"used\":true},\ + {\"PRN\":29,\"el\":67,\"az\":310,\"ss\":40,\"used\":true},\ + {\"PRN\":28,\"el\":59,\"az\":108,\"ss\":42,\"used\":true},\ + {\"PRN\":26,\"el\":51,\"az\":304,\"ss\":43,\"used\":true},\ + {\"PRN\":8,\"el\":44,\"az\":58,\"ss\":41,\"used\":true},\ + {\"PRN\":27,\"el\":16,\"az\":66,\"ss\":39,\"used\":true},\ + {\"PRN\":21,\"el\":10,\"az\":301,\"ss\":0,\"used\":false}]}"; + +/* Case 3: String list syntax */ + +static const char *json_str3 = "[\"foo\",\"bar\",\"baz\"]"; + +static char *stringptrs[3]; +static char stringstore[256]; +static int stringcount; + +/*@-type@*/ +static const struct json_array_t json_array_3 = { + .element_type = t_string, + .arr.strings.ptrs = stringptrs, + .arr.strings.store = stringstore, + .arr.strings.storelen = sizeof(stringstore), + .count = &stringcount, + .maxlen = sizeof(stringptrs)/sizeof(stringptrs[0]), +}; +/*@+type@*/ + +/* Case 4: test defaulting of unspecified attributes */ + +static const char *json_str4 = "{\"flag1\":true,\"flag2\":false}"; + +static bool flag1, flag2; +static double dftreal; +static int dftinteger; +static unsigned int dftuinteger; + +static const struct json_attr_t json_attrs_4[] = { + {"dftint", t_integer, .addr.integer = &dftinteger, .dflt.integer = -5}, + {"dftuint", t_integer, .addr.uinteger = &dftuinteger, .dflt.uinteger = 10}, + {"dftreal", t_real, .addr.real = &dftreal, .dflt.real = 23.17}, + {"flag1", t_boolean, .addr.boolean = &flag1,}, + {"flag2", t_boolean, .addr.boolean = &flag2,}, + {NULL}, +}; + +/* Case 5: test DEVICE parsing */ + +static const char *json_str5 = "{\"class\":\"DEVICE\",\ + \"path\":\"/dev/ttyUSB0\",\ + \"flags\":5,\ + \"driver\":\"Foonly\",\"subtype\":\"Foonly Frob\"\ + }"; + +/* Case 6: test parsing of subobject list into array of structures */ + +static const char *json_str6 = "{\"parts\":[\ + {\"name\":\"Urgle\", \"flag\":true, \"count\":3},\ + {\"name\":\"Burgle\",\"flag\":false,\"count\":1},\ + {\"name\":\"Witter\",\"flag\":true, \"count\":4},\ + {\"name\":\"Thud\", \"flag\":false,\"count\":1}]}"; + +struct dumbstruct_t { + char name[64]; + bool flag; + int count; +}; +static struct dumbstruct_t dumbstruck[5]; +static int dumbcount; + +/*@-type@*/ +static const struct json_attr_t json_attrs_6_subtype[] = { + {"name", t_string, .addr.offset = offsetof(struct dumbstruct_t, name), + .len = 64}, + {"flag", t_boolean, .addr.offset = offsetof(struct dumbstruct_t, flag),}, + {"count", t_integer, .addr.offset = offsetof(struct dumbstruct_t, count),}, + {NULL}, +}; + +static const struct json_attr_t json_attrs_6[] = { + {"parts", t_array, .addr.array.element_type = t_structobject, + .addr.array.arr.objects.base = (char*)&dumbstruck, + .addr.array.arr.objects.stride = sizeof(struct dumbstruct_t), + .addr.array.arr.objects.subtype = json_attrs_6_subtype, + .addr.array.count = &dumbcount, + .addr.array.maxlen = sizeof(dumbstruck)/sizeof(dumbstruck[0])}, + {NULL}, +}; +/*@+type@*/ + +/* Case 7: test parsing of version response */ + +static const char *json_str7 = "{\"class\":\"VERSION\",\ + \"release\":\"2.40dev\",\"rev\":\"dummy-revision\",\ + \"proto_major\":3,\"proto_minor\":1}"; + +/* Case 8: test parsing arrays of enumerated types */ + +static const char *json_str8 = "{\"fee\":\"FOO\",\"fie\":\"BAR\",\"foe\":\"BAZ\"}"; +static const struct json_enum_t enum_table[] = { + {"BAR", 6}, {"FOO", 3}, {"BAZ", 14}, {NULL} +}; + +static int fee, fie, foe; +static const struct json_attr_t json_attrs_8[] = { + {"fee", t_integer, .addr.integer = &fee, .map=enum_table}, + {"fie", t_integer, .addr.integer = &fie, .map=enum_table}, + {"foe", t_integer, .addr.integer = &foe, .map=enum_table}, + {NULL}, +}; + +/* Case 9: Like case 6 but w/ an empty array */ + +static const char *json_str9 = "{\"parts\":[]}"; + +/*@ +fullinitblock @*/ +/* *INDENT-ON* */ + +static void jsontest(int i) +{ + int status = 0; + + switch (i) + { + case 1: + status = libgps_json_unpack(json_str1, &gpsdata, NULL); + assert_case(1, status); + assert_string("device", gpsdata.dev.path, "GPS#1"); + assert_integer("mode", gpsdata.fix.mode, 3); + assert_real("time", gpsdata.fix.time, 1119168761.8900001); + assert_real("lon", gpsdata.fix.longitude, 46.498203637); + assert_real("lat", gpsdata.fix.latitude, 7.568074350); + break; + + case 2: + status = libgps_json_unpack(json_str2, &gpsdata, NULL); + assert_case(2, status); + assert_integer("used", gpsdata.satellites_used, 6); + assert_integer("PRN[0]", gpsdata.PRN[0], 10); + assert_integer("el[0]", gpsdata.elevation[0], 45); + assert_integer("az[0]", gpsdata.azimuth[0], 196); + assert_real("ss[0]", gpsdata.ss[0], 34); + assert_integer("used[0]", gpsdata.used[0], 10); + assert_integer("used[5]", gpsdata.used[5], 27); + assert_integer("PRN[6]", gpsdata.PRN[6], 21); + assert_integer("el[6]", gpsdata.elevation[6], 10); + assert_integer("az[6]", gpsdata.azimuth[6], 301); + assert_real("ss[6]", gpsdata.ss[6], 0); + break; + + case 3: + status = json_read_array(json_str3, &json_array_3, NULL); + assert_case(3, status); + assert(stringcount == 3); + assert(strcmp(stringptrs[0], "foo") == 0); + assert(strcmp(stringptrs[1], "bar") == 0); + assert(strcmp(stringptrs[2], "baz") == 0); + break; + + case 4: + status = json_read_object(json_str4, json_attrs_4, NULL); + assert_case(4, status); + assert_integer("dftint", dftinteger, -5); /* did the default work? */ + assert_uinteger("dftuint", dftuinteger, 10); /* did the default work? */ + assert_real("dftreal", dftreal, 23.17); /* did the default work? */ + assert_boolean("flag1", flag1, true); + assert_boolean("flag2", flag2, false); + break; + + case 5: + status = libgps_json_unpack(json_str5, &gpsdata, NULL); + assert_case(5, status); + assert_string("path", gpsdata.dev.path, "/dev/ttyUSB0"); + assert_integer("flags", gpsdata.dev.flags, 5); + assert_string("driver", gpsdata.dev.driver, "Foonly"); + break; + + case 6: + status = json_read_object(json_str6, json_attrs_6, NULL); + assert_case(6, status); + assert_integer("dumbcount", dumbcount, 4); + assert_string("dumbstruck[0].name", dumbstruck[0].name, "Urgle"); + assert_string("dumbstruck[1].name", dumbstruck[1].name, "Burgle"); + assert_string("dumbstruck[2].name", dumbstruck[2].name, "Witter"); + assert_string("dumbstruck[3].name", dumbstruck[3].name, "Thud"); + assert_boolean("dumbstruck[0].flag", dumbstruck[0].flag, true); + assert_boolean("dumbstruck[1].flag", dumbstruck[1].flag, false); + assert_boolean("dumbstruck[2].flag", dumbstruck[2].flag, true); + assert_boolean("dumbstruck[3].flag", dumbstruck[3].flag, false); + assert_integer("dumbstruck[0].count", dumbstruck[0].count, 3); + assert_integer("dumbstruck[1].count", dumbstruck[1].count, 1); + assert_integer("dumbstruck[2].count", dumbstruck[2].count, 4); + assert_integer("dumbstruck[3].count", dumbstruck[3].count, 1); + break; + + case 7: + status = libgps_json_unpack(json_str7, &gpsdata, NULL); + assert_case(7, status); + assert_string("release", gpsdata.version.release, "2.40dev"); + assert_string("rev", gpsdata.version.rev, "dummy-revision"); + assert_integer("proto_major", gpsdata.version.proto_major, 3); + assert_integer("proto_minor", gpsdata.version.proto_minor, 1); + break; + + case 8: + status = json_read_object(json_str8, json_attrs_8, NULL); + assert_case(8, status); + assert_integer("fee", fee, 3); + assert_integer("fie", fie, 6); + assert_integer("foe", foe, 14); + break; + + case 9: + /* yes, the '6' in the next line is correct */ + status = json_read_object(json_str9, json_attrs_6, NULL); + assert_case(9, status); + assert_integer("dumbcount", dumbcount, 0); + break; + +#define MAXTEST 9 + + default: + (int)fputs("Unknown test number\n", stderr); + break; + } +} + +int main(int argc, char *argv[]) +{ + int option; + int individual = 0; + + while ((option = getopt(argc, argv, "hn:D:?")) != -1) { + switch (option) { + case 'D': + //gps_enable_debug(atoi(optarg), stdout); + break; + case 'n': + individual = atoi(optarg); + break; + case '?': + case 'h': + default: + (void)fputs("usage: test_json [-D lvl]\n", stderr); + exit(EXIT_FAILURE); + } + } + + (void)fprintf(stderr, "JSON unit test "); + + if (individual) + jsontest(individual); + else { + int i; + for (i = 1; i <= MAXTEST; i++) + jsontest(i); + } + + (void)fprintf(stderr, "succeeded.\n"); + + exit(EXIT_SUCCESS); +}