// vi: ts=4 sw=4 noet: /* ================================================================================== Copyright (c) 2020 Nokia Copyright (c) 2020 AT&T Intellectual Property. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================== */ /* Mnemonic: jwrapper.c Abstract: A wrapper interface to the jsmn library which makes it a bit easier to use. Parses a json string capturing the contents in a symtab. This code is based on the AT&T VFd open source library available on github.com/att/vfd. The changes are mostly to port to the RMR version of symtab from VFd's version. Author: E. Scott Daniels Date: 26 June 2020 */ #include #include #include #include #ifndef DEBUG #define DEBUG 0 #endif #define JSMN_STATIC 1 // jsmn no longer builds into a library; this pulls as static functions #include //#include <../../ext/jsmn/jsmn.h> #include #define JSON_SYM_NAME "_jw_json_string" #define MAX_THINGS 1024 * 4 // max objects/elements #define PT_UNKNOWN 0 // primative types; unknown for non prim #define PT_VALUE 1 #define PT_BOOL 2 #define PT_NULL 3 #define PT_STRING 4 #define OBJ_SPACE 1 // space in the symbol table where json bits are stashed #define MGT_SPACE 2 // non-json objects in the hash (management things) extern void jw_nuke( void* st ); // --------------------------------------------------------------------------------------- /* This is what we will manage in the symtab. Right now we store all values (primatives) as double, but we could be smarter about it and look for a decimal. Unsigned and differences between long, long long etc are tough. */ typedef struct jthing { int jsmn_type; // propigated type from jsmn (jsmn constants) int prim_type; // finer grained primative type (bool, null, value) int nele; // number of elements if applies union { double fv; void *pv; } v; } jthing_t; /* Given the json token, 'extract' the element by marking the end with a nil character, and returning a pointer to the start. We do this so that we don't create a bunch of small buffers that must be found and freed; we can just release the json string and we'll be done (read won't leak). */ static char* extract( char* buf, jsmntok_t *jtoken ) { buf[jtoken->end] = 0; return &buf[jtoken->start]; } /* create a new jthing and add a reference to it in the symbol table st. sets the number of elements to 1 by default. */ static jthing_t *mk_thing( void *st, char *name, int jsmn_type ) { jthing_t *jtp = NULL; if( st != NULL && name != NULL && (jtp = (jthing_t *) malloc( sizeof( *jtp ) )) != NULL ) { if( DEBUG ) { fprintf( stderr, " jwrapper adding: %s type=%d\n", name, jsmn_type ); } jtp->jsmn_type = jsmn_type; jtp->prim_type = PT_UNKNOWN; // caller must set this jtp->nele = 1; jtp->v.fv = 0; rmr_sym_put( st, name, OBJ_SPACE, jtp ); } else { fprintf( stderr, "[WARN] jwrapper: unable to create '%s' type=%d\n", name, jsmn_type ); } return jtp; } /* Find the named array. Returns a pointer to the jthing that represents the array (type, size and pointer to actual array of jthings). Returns nil pointer if the named thing isn't there or isn't an array. */ static jthing_t* suss_array( void* st, const char* name ) { jthing_t* jtp = NULL; // thing that is referenced by the symtab if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { jtp = jtp->jsmn_type == JSMN_ARRAY ? jtp : NULL; } return jtp; } /* Suss an array from the hash and return the ith element. */ static jthing_t* suss_element( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab jthing_t* jarray; jthing_t* rv = NULL; if( (jtp = suss_array( st, name )) != NULL && // have pointer idx >= 0 && // and in range idx < jtp->nele ) { if( (jarray = jtp->v.pv) != NULL ) { rv = &jarray[idx]; } } return rv; } /* Invoked for each thing in the symtab; we free the things that actually point to allocated data (e.g. arrays) and recurse to handle objects. */ static void nix_things( void* st, void* se, const char* name, void* ele, void *data ) { jthing_t* j; jthing_t* jarray; int i; j = (jthing_t *) ele; if( j ) { switch( j->jsmn_type ) { case JSMN_ARRAY: if( (jarray = (jthing_t *) j->v.pv) != NULL ) { for( i = 0; i < j->nele; i++ ) { // must look for embedded objects if( jarray[i].jsmn_type == JSMN_OBJECT ) { jw_nuke( jarray[i].v.pv ); jarray[i].jsmn_type = JSMN_UNDEFINED; // prevent accidents } } free( j->v.pv ); // must free the array (arrays aren't nested, so all things in the array don't reference allocated mem) free( j ); } break; case JSMN_OBJECT: // delete the sub symtab jw_nuke( j->v.pv ); j->jsmn_type = JSMN_UNDEFINED; // prevent a double free free( j ); break; case JSMN_STRING: case JSMN_PRIMITIVE: free( j ); break; } } } /* Nix non-json things that are also in the hash. */ static void nix_mgt( void* st, void* se, const char* name, void* ele, void *data ) { free( ele ); } /* Invoked for each thing and prints what we can to stderr. */ static void dump_things( void* st, void* se, const char* name, void* ele, void *data ) { jthing_t* j; jthing_t* jarray; int i; j = (jthing_t *) ele; if( j ) { fprintf( stderr, " jwrapper: element '%s' has ptype %d, jsmn type %d\n", name, j->prim_type, j->jsmn_type ); } else { fprintf( stderr, " jwrapper: element has no data: '%s'\n", name ); } } /* Real work for parsing an object ({...}) from the json. Called by jw_new() and recurses to deal with sub-objects. */ void* parse_jobject( void* st, char *json, char* prefix ) { jthing_t *jtp; // json thing that we just created int i; int n; char *name; // name in the json char *data; // data string from the json jthing_t* jarray; // array of jthings we'll coonstruct int size; int osize; int njtokens; // tokens actually sussed out jsmn_parser jp; // 'parser' object jsmntok_t *jtokens; // pointer to tokens returned by the parser char pname[1024]; // name with prefix char wbuf[256]; // temp buf to build a working name in char* dstr; // dup'd string jsmn_init( &jp ); // does this have a failure mode? jtokens = (jsmntok_t *) malloc( sizeof( *jtokens ) * MAX_THINGS ); if( jtokens == NULL ) { fprintf( stderr, "[CRI] jwrapper: cannot allocate tokens array\n" ); return NULL; } njtokens = jsmn_parse( &jp, json, strlen( json ), jtokens, MAX_THINGS ); if( jtokens[0].type != JSMN_OBJECT ) { // if it's not an object then we can't parse it. fprintf( stderr, "[WARN] jwrapper: badly formed json; initial opening bracket ({) not detected\n" ); rmr_sym_free( st ); free( jtokens ); return NULL; } if( DEBUG ) { for( i = 1; i < njtokens-1; i++ ) { fprintf( stderr, " %4d: size=%d start=%d end=%d %s\n", i, jtokens[i].size, jtokens[i].start, jtokens[i].end, extract( json, &jtokens[i] ) ); } } for( i = 1; i < njtokens-1; i++ ) { // we'll silently skip the last token if it's "name" without a value if( jtokens[i].type != JSMN_STRING ) { fprintf( stderr, "[WARN] jwrapper: badly formed json [%d]; expected name (string) found type=%d %s\n", i, jtokens[i].type, extract( json, &jtokens[i] ) ); rmr_sym_free( st ); free( jtokens ); return NULL; } name = extract( json, &jtokens[i] ); if( *prefix != 0 ) { snprintf( pname, sizeof( pname ), "%s.%s", prefix, name ); name = pname; } size = jtokens[i].size; i++; // at the data token now switch( jtokens[i].type ) { case JSMN_OBJECT: // save object in two ways: as an object 'blob' and in the current symtab using name as a base (original) dstr = strdup( extract( json, &jtokens[i] ) ); snprintf( wbuf, sizeof( wbuf ), "%s_json", name ); // must stash the json string in the symtab for clean up during nuke rmr_sym_put( st, wbuf, MGT_SPACE, dstr ); parse_jobject( st, dstr, name ); // recurse to add the object as objectname.xxxx elements if( (jtp = mk_thing( st, name, jtokens[i].type )) != NULL && // create thing and reference it in current symtab (jtp->v.pv = (void *) rmr_sym_alloc( 255 ) ) != NULL ) { // object is just a blob dstr = strdup( extract( json, &jtokens[i] ) ); rmr_sym_put( jtp->v.pv, JSON_SYM_NAME, MGT_SPACE, dstr ); // must stash json so it is freed during nuke() parse_jobject( jtp->v.pv, dstr, "" ); // recurse acorss the string and build a new symtab size = jtokens[i].end; // done with them, we need to skip them i++; while( i < njtokens-1 && jtokens[i].end < size ) { if( DEBUG ){ fprintf( stderr, "\tskip: [%d] object element start=%d end=%d (%s)\n", i, jtokens[i].start, jtokens[i].end, extract( json, &jtokens[i]) ); } i++; } i--; // must allow loop to bump past the last } break; case JSMN_ARRAY: size = jtokens[i].size; // size is burried here, and not with the name jtp = mk_thing( st, name, jtokens[i].type ); i++; // skip first ele; it is the whole array string which I don't grock the need for, but it's their code... if( jtp == NULL ) { fprintf( stderr, "[WARN] jwrapper: memory alloc error processing element [%d] in json\n", i ); rmr_sym_free( st ); free( jtokens ); return NULL; } jarray = jtp->v.pv = (jsmntok_t *) malloc( sizeof( *jarray ) * size ); // allocate the array memset( jarray, 0, sizeof( *jarray ) * size ); jtp->nele = size; for( n = 0; n < size; n++ ) { // for each array element jarray[n].prim_type = PT_UNKNOWN; // assume not primative type switch( jtokens[i+n].type ) { case JSMN_OBJECT: jarray[n].v.pv = (void *) rmr_sym_alloc( 255 ); if( jarray[n].v.pv != NULL ) { jarray[n].jsmn_type = JSMN_OBJECT; parse_jobject( jarray[n].v.pv, extract( json, &jtokens[i+n] ), "" ); // recurse acorss the string and build a new symtab osize = jtokens[i+n].end; // done with them, we need to skip them i++; while( i+n < njtokens-1 && jtokens[n+i].end < osize ) { i++; } i--; // allow incr at loop end } break; case JSMN_ARRAY: fprintf( stderr, "[WARN] jwrapper: [%d] array element %d is not valid type (array) is not string or primative\n", i, n ); n += jtokens[i+n].size; // this should skip the nested array free( jtp ); free( jarray ); jarray = NULL; break; case JSMN_STRING: data = extract( json, &jtokens[i+n] ); jarray[n].v.pv = (void *) data; jarray[n].prim_type = PT_STRING; jarray[n].jsmn_type = JSMN_STRING; break; case JSMN_PRIMITIVE: data = extract( json, &jtokens[i+n] ); switch( *data ) { case 'T': case 't': jarray[n].v.fv = 1; jarray[n].prim_type = PT_BOOL; break; case 'F': case 'f': jarray[n].prim_type = PT_BOOL; jarray[n].v.fv = 0; break; case 'N': // assume null, nil, or some variant case 'n': jarray[n].prim_type = PT_NULL; jarray[n].v.fv = 0; break; default: jarray[n].prim_type = PT_VALUE; jarray[n].v.fv = strtod( data, NULL ); // store all numerics as double break; } jarray[n].jsmn_type = JSMN_PRIMITIVE; break; case JSMN_UNDEFINED: // fallthrough default: fprintf( stderr, "[WARN] jwrapper: [%d] array element %d is not valid type (unknown=%d) is not string or primative\n", i, n, jtokens[i].type ); rmr_sym_free( st ); free( jtokens ); return NULL; break; } } i += size - 1; // must allow loop to push to next break; case JSMN_STRING: data = extract( json, &jtokens[i] ); if( (jtp = mk_thing( st, name, jtokens[i].type )) != NULL ) { jtp->prim_type = PT_STRING; jtp->v.pv = (void *) data; // just point into the large json string } break; case JSMN_PRIMITIVE: data = extract( json, &jtokens[i] ); if( (jtp = mk_thing( st, name, jtokens[i].type )) != NULL ) { switch( *data ) { // assume T|t is true and F|f is false case 'T': case 't': jtp->prim_type = PT_BOOL; jtp->v.fv = 1; break; case 'F': case 'f': jtp->prim_type = PT_BOOL; jtp->v.fv = 0; break; case 'N': // Null or some form of that case 'n': jtp->prim_type = PT_NULL; jtp->v.fv = 0; break; default: jtp->prim_type = PT_VALUE; jtp->v.fv = strtod( data, NULL ); // store all numerics as double break; } } break; default: fprintf( stderr, "[WARN] jwrapper: element [%d] is undefined or of unknown type\n", i ); break; } } free( jtokens ); return st; } // --------------- public functions ----------------------------------------------------------------- /* Destroy all operating structures assocaited with the symtab pointer passed in. */ extern void jw_nuke( void* st ) { char* buf; // pointer to the original json to free if( st != NULL ) { rmr_sym_foreach_class( st, OBJ_SPACE, nix_things, NULL ); // free any json thing that the symtab references rmr_sym_foreach_class( st, MGT_SPACE, nix_mgt, NULL ); // free management things rmr_sym_free( st ); // free the symtab itself } } /* Scan the given st and write some useful (?) info to stderr. */ extern void jw_dump( void* st ) { if( st != NULL ) { rmr_sym_foreach_class( st, OBJ_SPACE, dump_things, NULL ); } else { fprintf( stderr, " jwrapper: dump: no table\n" ); } } /* Given a json string, parse it, and put the things into a symtab. return the symtab pointer to the caller. They pass the symtab pointer back to the various get functions. This is the entry point. It sets up the symbol table and invokes the parse object funtion to start at the first level. Parse object will recurse for nested objects if present. */ extern void* jw_new( const char* json ) { void *st = NULL; // symbol table char* djson; // dup so we can save it void* rp = NULL; // return value if( json != NULL && (st = rmr_sym_alloc( MAX_THINGS )) != NULL ) { djson = strdup( json ); // allows user to free/overlay their buffer as needed rp = parse_jobject( st, djson, "" ); // empty prefix for the root object; parse_jo will clean up and free st if( rp == NULL ) { free( djson ); } else { rmr_sym_put( st, (unsigned char *) JSON_SYM_NAME, MGT_SPACE, djson ); // must have a reference to the string until symtab is trashed } } return rp; } /* Returns true (1) if the named field is missing. */ extern int jw_missing( void* st, const char* name ) { int rv = 0; if( st != NULL && name != NULL ) { rv = rmr_sym_get( st, name, OBJ_SPACE ) == NULL; } return rv; } /* Returns true (1) if the named field is in the blob; */ extern int jw_exists( void* st, const char* name ) { int rv = 0; if( st != NULL && name != NULL ) { rv = rmr_sym_get( st, name, OBJ_SPACE ) != NULL; } return rv; } /* Returns true (1) if the primative type is value (double). */ extern int jw_is_value( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab int rv = 0; if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { rv = jtp->prim_type == PT_VALUE; } return rv; } /* Returns true (1) if the primative type is string. */ extern int jw_is_string( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab int rv = 0; if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { rv = jtp->prim_type == PT_STRING; } return rv; } /* Returns true (1) if the primative type is boolean. */ extern int jw_is_bool( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab int rv = 0; if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { rv = jtp->prim_type == PT_BOOL; } return rv; } /* Returns true (1) if the primative type was a 'null' type. */ extern int jw_is_null( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab int rv = 0; if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { rv = jtp->prim_type == PT_NULL; } return rv; } /* Look up the name in the symtab and return the string (data). */ extern char* jw_string( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab char* rv = NULL; if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { if( jtp->jsmn_type == JSMN_STRING ) { rv = (char *) jtp->v.pv; } } return rv; } /* Look up name and return the value. */ extern double jw_value( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab double rv = 0.0; if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { if( jtp->jsmn_type == JSMN_PRIMITIVE ) { rv = jtp->v.fv; } } return rv; } /* Look up name and return the blob (symtab). */ extern void* jw_blob( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab void* rv = NULL; if( st != NULL && name != NULL && (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { if( jtp->jsmn_type == JSMN_OBJECT ) { rv = (void *) jtp->v.pv; } } return rv; } /* Look up the element and return boolean state; This takes the C approach and returns true/false based on the value. */ extern int jw_bool_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry int rv = 0; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { rv = !! ((int) jtp->v.fv); } return rv; } /* Look up array element as a string. Returns NULL if: name is not an array name is not in the hash index is out of range element is not a string */ extern char* jw_string_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry char* rv = NULL; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { if( jtp->jsmn_type == JSMN_STRING ) { rv = (char *) jtp->v.pv; } } return rv; } /* Look up array element as a value. Returns 0 if: name is not an array name is not in the hash index is out of range element is not a value */ extern double jw_value_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry double rv = 0.0; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { if( jtp->prim_type == PT_VALUE ) { rv = jtp->v.fv; } } return rv; } /* Look up the element and check to see if it is a string. Return true (1) if it is. */ extern int jw_is_string_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry int rv = 0; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { rv = jtp->prim_type == PT_STRING; } return rv; } /* Look up the element and check to see if it is a value primative. Return true (1) if it is. */ extern int jw_is_value_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry int rv = 0; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { rv = jtp->prim_type == PT_VALUE; } return rv; } /* Look up the element and check to see if it is a boolean primative. Return true (1) if it is. */ extern int jw_is_bool_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry int rv = 0; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { rv = jtp->prim_type == PT_BOOL; } return rv; } /* Look up the element and check to see if it is a null primative. Return true (1) if it is. */ extern int jw_is_null_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry int rv = 0; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { rv = jtp->prim_type == PT_NULL; } return rv; } /* Look up array element as an object. Returns NULL if: name is not an array name is not in the hash index is out of range element is not an object An object in an array is a standalone symbol table. Thus the object is treated differently than a nested object whose members are a part of the parent namespace. An object in an array has its own namespace. */ extern void* jw_obj_ele( void* st, const char* name, int idx ) { jthing_t* jtp; // thing that is referenced by the symtab entry void* rv = NULL; if( st != NULL && name != NULL && (jtp = suss_element( st, name, idx )) != NULL ) { if( jtp->jsmn_type == JSMN_OBJECT ) { rv = (void *) jtp->v.pv; } } return rv; } /* Return the size of the array named. Returns -1 if the thing isn't an array, and returns the number of elements otherwise. */ extern int jw_array_len( void* st, const char* name ) { jthing_t* jtp; // thing that is referenced by the symtab entry int rv = -1; if( st != NULL && name != NULL && (jtp = suss_array( st, name )) != NULL ) { rv = jtp->nele; } return rv; }