2 * Copyright (c) 2015 - 2016 Svyatoslav Mishyn <juef@openmailbox.org>
3 * Copyright (c) 2019 Steve Bennett <steveb@workware.net.au>
5 * Permission to use, copy, modify, and/or distribute this software for
6 * any purpose with or without fee is hereby granted, provided that the
7 * above copyright notice and this permission notice appear in all
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
11 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
12 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
13 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
14 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
15 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
16 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17 * PERFORMANCE OF THIS SOFTWARE.
27 #include "jsmn/jsmn.h"
29 /* These are all the schema types we support */
45 /* The following are used for -schema */
49 Jim_Obj
*schemaTypeObj
[JSON_MAX_TYPE
];
52 static void json_decode_dump_value(Jim_Interp
*interp
, struct json_state
*state
, Jim_Obj
*list
);
55 * Start a new subschema. Returns the previous schemaObj.
56 * Does nothing and returns NULL if -schema is not enabled.
58 static Jim_Obj
*json_decode_schema_push(Jim_Interp
*interp
, struct json_state
*state
)
60 Jim_Obj
*prevSchemaObj
= NULL
;
61 if (state
->enable_schema
) {
62 prevSchemaObj
= state
->schemaObj
;
63 state
->schemaObj
= Jim_NewListObj(interp
, NULL
, 0);
64 Jim_IncrRefCount(state
->schemaObj
);
70 * Combines the current schema with the previous schema, prevSchemaObj
71 * returned by json_decode_schema_push().
72 * Does nothing if -schema is not enabled.
74 static void json_decode_schema_pop(Jim_Interp
*interp
, struct json_state
*state
, Jim_Obj
*prevSchemaObj
)
76 if (state
->enable_schema
) {
77 Jim_ListAppendElement(interp
, prevSchemaObj
, state
->schemaObj
);
78 Jim_DecrRefCount(interp
, state
->schemaObj
);
79 state
->schemaObj
= prevSchemaObj
;
84 * Appends the schema type to state->schemaObj based on 'type'
86 static void json_decode_add_schema_type(Jim_Interp
*interp
, struct json_state
*state
, json_schema_t type
)
88 static const char * const schema_names
[] = {
96 assert(type
>= 0 && type
< JSON_MAX_TYPE
);
97 /* Share multiple instances of the same type */
98 if (state
->schemaTypeObj
[type
] == NULL
) {
99 state
->schemaTypeObj
[type
] = Jim_NewStringObj(interp
, schema_names
[type
], -1);
101 Jim_ListAppendElement(interp
, state
->schemaObj
, state
->schemaTypeObj
[type
]);
105 * Returns the schema type for the given token.
106 * There is a one-to-one correspondence except for JSMN_PRIMITIVE
107 * which will return JSON_BOOL for true, false and JSON_NUM otherise.
109 static json_schema_t
json_decode_get_type(const jsmntok_t
*tok
, const char *json
)
114 if (json
[tok
->start
] == 't' || json
[tok
->start
] == 'f') {
121 /* Return mixed by default - need other checks to select list instead */
130 * Returns the current object (state->tok) as a Tcl list.
132 * state->tok is incremented to just past the object that was dumped.
135 json_decode_dump_container(Jim_Interp
*interp
, struct json_state
*state
)
138 Jim_Obj
*list
= Jim_NewListObj(interp
, NULL
, 0);
139 int size
= state
->tok
->size
;
140 int type
= state
->tok
->type
;
141 json_schema_t container_type
= JSON_OBJ
; /* JSON_LIST, JSON_MIXED or JSON_OBJ */
143 if (state
->schemaObj
) {
144 json_schema_t list_type
;
145 /* Figure out the type to use for the container */
146 if (type
== JSMN_ARRAY
) {
147 /* If every element of the array is of the same primitive schema type (str, bool or num),
148 * we can use "list", otherwise need to use "mixed"
150 container_type
= JSON_LIST
;
152 list_type
= json_decode_get_type(&state
->tok
[1], state
->json
);
154 if (list_type
== JSON_BOOL
|| list_type
== JSON_STR
|| list_type
== JSON_NUM
) {
155 for (i
= 2; i
<= size
; i
++) {
156 if (json_decode_get_type(state
->tok
+ i
, state
->json
) != list_type
) {
158 container_type
= JSON_MIXED
;
164 container_type
= JSON_MIXED
;
168 json_decode_add_schema_type(interp
, state
, container_type
);
169 if (container_type
== JSON_LIST
&& size
) {
170 json_decode_add_schema_type(interp
, state
, list_type
);
176 for (i
= 0; i
< size
; i
++) {
177 if (type
== JSMN_OBJECT
) {
178 /* Dump the object key */
179 if (state
->enable_schema
) {
180 const char *p
= state
->json
+ state
->tok
->start
;
181 int len
= state
->tok
->end
- state
->tok
->start
;
182 Jim_ListAppendElement(interp
, state
->schemaObj
, Jim_NewStringObj(interp
, p
, len
));
184 json_decode_dump_value(interp
, state
, list
);
187 if (state
->enable_index
&& type
== JSMN_ARRAY
) {
188 Jim_ListAppendElement(interp
, list
, Jim_NewIntObj(interp
, i
));
191 if (state
->schemaObj
&& container_type
!= JSON_LIST
) {
192 if (state
->tok
->type
== JSMN_STRING
|| state
->tok
->type
== JSMN_PRIMITIVE
) {
193 json_decode_add_schema_type(interp
, state
, json_decode_get_type(state
->tok
, state
->json
));
197 /* Dump the array or object value */
198 json_decode_dump_value(interp
, state
, list
);
205 * Appends the value at state->tok to 'list' and increments state->tok to just
208 * Also appends to the schema if state->enable_schema is set.
211 json_decode_dump_value(Jim_Interp
*interp
, struct json_state
*state
, Jim_Obj
*list
)
213 const jsmntok_t
*t
= state
->tok
;
215 if (t
->type
== JSMN_STRING
|| t
->type
== JSMN_PRIMITIVE
) {
217 int len
= t
->end
- t
->start
;
218 const char *p
= state
->json
+ t
->start
;
219 if (t
->type
== JSMN_STRING
) {
220 /* Do we need to process backslash escapes? */
221 if (state
->need_subst
== 0 && memchr(p
, '\\', len
) != NULL
) {
222 state
->need_subst
= 1;
224 elem
= Jim_NewStringObj(interp
, p
, len
);
225 } else if (p
[0] == 'n') { /* null */
226 elem
= state
->nullObj
;
227 } else if (p
[0] == 'I') {
228 elem
= Jim_NewStringObj(interp
, "Inf", -1);
229 } else if (p
[0] == '-' && p
[1] == 'I') {
230 elem
= Jim_NewStringObj(interp
, "-Inf", -1);
231 } else { /* number, true or false */
232 elem
= Jim_NewStringObj(interp
, p
, len
);
235 Jim_ListAppendElement(interp
, list
, elem
);
239 Jim_Obj
*prevSchemaObj
= json_decode_schema_push(interp
, state
);
240 Jim_Obj
*newList
= json_decode_dump_container(interp
, state
);
241 Jim_ListAppendElement(interp
, list
, newList
);
242 json_decode_schema_pop(interp
, state
, prevSchemaObj
);
246 /* Parses the options ?-null string? ?-schema? *state.
247 * Any options not present are not set.
249 * Returns JIM_OK or JIM_ERR and sets an error result.
251 static int parse_json_decode_options(Jim_Interp
*interp
, int argc
, Jim_Obj
*const argv
[], struct json_state
*state
)
253 static const char * const options
[] = { "-index", "-null", "-schema", NULL
};
254 enum { OPT_INDEX
, OPT_NULL
, OPT_SCHEMA
, };
257 for (i
= 1; i
< argc
- 1; i
++) {
259 if (Jim_GetEnum(interp
, argv
[i
], options
, &option
, NULL
, JIM_ERRMSG
| JIM_ENUM_ABBREV
) != JIM_OK
) {
264 state
->enable_index
= 1;
269 Jim_IncrRefCount(argv
[i
]);
270 Jim_DecrRefCount(interp
, state
->nullObj
);
271 state
->nullObj
= argv
[i
];
275 state
->enable_schema
= 1;
281 Jim_WrongNumArgs(interp
, 1, argv
,
282 "?-index? ?-null nullvalue? ?-schema? json");
290 * Use jsmn to tokenise the JSON string 'json' of length 'len'
292 * Returns an allocated array of tokens or NULL on error (and sets an error result)
295 json_decode_tokenize(Jim_Interp
*interp
, const char *json
, size_t len
)
301 /* Parse once just to find the number of tokens */
303 n
= jsmn_parse(&parser
, json
, len
, NULL
, 0);
307 case JSMN_ERROR_INVAL
:
308 Jim_SetResultString(interp
, "invalid JSON string", -1);
311 case JSMN_ERROR_PART
:
312 Jim_SetResultString(interp
, "truncated JSON string", -1);
316 Jim_SetResultString(interp
, "root element must be an object or an array", -1);
327 t
= Jim_Alloc(n
* sizeof(*t
));
330 n
= jsmn_parse(&parser
, json
, len
, t
, n
);
331 if (t
->type
!= JSMN_OBJECT
&& t
->type
!= JSMN_ARRAY
) {
343 * json::decode returns the decoded data structure.
345 * If -schema is specified, returns a list of {data schema}
348 json_decode(Jim_Interp
*interp
, int argc
, Jim_Obj
*const argv
[])
354 struct json_state state
;
356 memset(&state
, 0, sizeof(state
));
358 state
.nullObj
= Jim_NewStringObj(interp
, "null", -1);
359 Jim_IncrRefCount(state
.nullObj
);
361 if (parse_json_decode_options(interp
, argc
, argv
, &state
) != JIM_OK
) {
365 state
.json
= Jim_GetString(argv
[argc
- 1], &len
);
368 Jim_SetResultString(interp
, "empty JSON string", -1);
371 if ((tokens
= json_decode_tokenize(interp
, state
.json
, len
)) == NULL
) {
375 json_decode_schema_push(interp
, &state
);
377 list
= json_decode_dump_container(interp
, &state
);
381 /* Make sure the refcount doesn't go to 0 during Jim_SubstObj() */
382 Jim_IncrRefCount(list
);
384 if (state
.need_subst
) {
385 /* Subsitute backslashes in the returned dictionary.
386 * Need to be careful of refcounts.
387 * Note that Jim_SubstObj() supports a few more escapes than
388 * JSON requires, but should give the same result for all legal escapes.
391 Jim_SubstObj(interp
, list
, &newList
, JIM_SUBST_FLAG
| JIM_SUBST_NOCMD
| JIM_SUBST_NOVAR
);
392 Jim_IncrRefCount(newList
);
393 Jim_DecrRefCount(interp
, list
);
397 if (state
.schemaObj
) {
398 Jim_Obj
*resultObj
= Jim_NewListObj(interp
, NULL
, 0);
399 Jim_ListAppendElement(interp
, resultObj
, list
);
400 Jim_ListAppendElement(interp
, resultObj
, state
.schemaObj
);
401 Jim_SetResult(interp
, resultObj
);
402 Jim_DecrRefCount(interp
, state
.schemaObj
);
405 Jim_SetResult(interp
, list
);
407 Jim_DecrRefCount(interp
, list
);
410 Jim_DecrRefCount(interp
, state
.nullObj
);
416 Jim_jsonInit(Jim_Interp
*interp
)
418 if (Jim_PackageProvide(interp
, "json", "1.0", JIM_ERRMSG
) != JIM_OK
) {
422 Jim_CreateCommand(interp
, "json::decode", json_decode
, NULL
, NULL
);
423 /* Load the Tcl implementation of the json encoder if possible */
424 Jim_PackageRequire(interp
, "jsonencode", 0);