Mercurial > malikania
comparison extern/jansson/src/pack_unpack.c @ 0:8991989c4708
Initial import
author | David Demelier <markand@malikania.fr> |
---|---|
date | Tue, 22 Mar 2016 18:26:05 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:8991989c4708 |
---|---|
1 /* | |
2 * Copyright (c) 2009-2014 Petri Lehtinen <petri@digip.org> | |
3 * Copyright (c) 2011-2012 Graeme Smecher <graeme.smecher@mail.mcgill.ca> | |
4 * | |
5 * Jansson is free software; you can redistribute it and/or modify | |
6 * it under the terms of the MIT license. See LICENSE for details. | |
7 */ | |
8 | |
9 #include <string.h> | |
10 #include "jansson.h" | |
11 #include "jansson_private.h" | |
12 #include "utf.h" | |
13 | |
14 typedef struct { | |
15 int line; | |
16 int column; | |
17 size_t pos; | |
18 char token; | |
19 } token_t; | |
20 | |
21 typedef struct { | |
22 const char *start; | |
23 const char *fmt; | |
24 token_t prev_token; | |
25 token_t token; | |
26 token_t next_token; | |
27 json_error_t *error; | |
28 size_t flags; | |
29 int line; | |
30 int column; | |
31 size_t pos; | |
32 } scanner_t; | |
33 | |
34 #define token(scanner) ((scanner)->token.token) | |
35 | |
36 static const char * const type_names[] = { | |
37 "object", | |
38 "array", | |
39 "string", | |
40 "integer", | |
41 "real", | |
42 "true", | |
43 "false", | |
44 "null" | |
45 }; | |
46 | |
47 #define type_name(x) type_names[json_typeof(x)] | |
48 | |
49 static const char unpack_value_starters[] = "{[siIbfFOon"; | |
50 | |
51 | |
52 static void scanner_init(scanner_t *s, json_error_t *error, | |
53 size_t flags, const char *fmt) | |
54 { | |
55 s->error = error; | |
56 s->flags = flags; | |
57 s->fmt = s->start = fmt; | |
58 memset(&s->prev_token, 0, sizeof(token_t)); | |
59 memset(&s->token, 0, sizeof(token_t)); | |
60 memset(&s->next_token, 0, sizeof(token_t)); | |
61 s->line = 1; | |
62 s->column = 0; | |
63 s->pos = 0; | |
64 } | |
65 | |
66 static void next_token(scanner_t *s) | |
67 { | |
68 const char *t; | |
69 s->prev_token = s->token; | |
70 | |
71 if(s->next_token.line) { | |
72 s->token = s->next_token; | |
73 s->next_token.line = 0; | |
74 return; | |
75 } | |
76 | |
77 t = s->fmt; | |
78 s->column++; | |
79 s->pos++; | |
80 | |
81 /* skip space and ignored chars */ | |
82 while(*t == ' ' || *t == '\t' || *t == '\n' || *t == ',' || *t == ':') { | |
83 if(*t == '\n') { | |
84 s->line++; | |
85 s->column = 1; | |
86 } | |
87 else | |
88 s->column++; | |
89 | |
90 s->pos++; | |
91 t++; | |
92 } | |
93 | |
94 s->token.token = *t; | |
95 s->token.line = s->line; | |
96 s->token.column = s->column; | |
97 s->token.pos = s->pos; | |
98 | |
99 t++; | |
100 s->fmt = t; | |
101 } | |
102 | |
103 static void prev_token(scanner_t *s) | |
104 { | |
105 s->next_token = s->token; | |
106 s->token = s->prev_token; | |
107 } | |
108 | |
109 static void set_error(scanner_t *s, const char *source, const char *fmt, ...) | |
110 { | |
111 va_list ap; | |
112 va_start(ap, fmt); | |
113 | |
114 jsonp_error_vset(s->error, s->token.line, s->token.column, s->token.pos, | |
115 fmt, ap); | |
116 | |
117 jsonp_error_set_source(s->error, source); | |
118 | |
119 va_end(ap); | |
120 } | |
121 | |
122 static json_t *pack(scanner_t *s, va_list *ap); | |
123 | |
124 | |
125 /* ours will be set to 1 if jsonp_free() must be called for the result | |
126 afterwards */ | |
127 static char *read_string(scanner_t *s, va_list *ap, | |
128 const char *purpose, size_t *out_len, int *ours) | |
129 { | |
130 char t; | |
131 strbuffer_t strbuff; | |
132 const char *str; | |
133 size_t length; | |
134 | |
135 next_token(s); | |
136 t = token(s); | |
137 prev_token(s); | |
138 | |
139 if(t != '#' && t != '%' && t != '+') { | |
140 /* Optimize the simple case */ | |
141 str = va_arg(*ap, const char *); | |
142 | |
143 if(!str) { | |
144 set_error(s, "<args>", "NULL string argument"); | |
145 return NULL; | |
146 } | |
147 | |
148 length = strlen(str); | |
149 | |
150 if(!utf8_check_string(str, length)) { | |
151 set_error(s, "<args>", "Invalid UTF-8 %s", purpose); | |
152 return NULL; | |
153 } | |
154 | |
155 *out_len = length; | |
156 *ours = 0; | |
157 return (char *)str; | |
158 } | |
159 | |
160 strbuffer_init(&strbuff); | |
161 | |
162 while(1) { | |
163 str = va_arg(*ap, const char *); | |
164 if(!str) { | |
165 set_error(s, "<args>", "NULL string argument"); | |
166 strbuffer_close(&strbuff); | |
167 return NULL; | |
168 } | |
169 | |
170 next_token(s); | |
171 | |
172 if(token(s) == '#') { | |
173 length = va_arg(*ap, int); | |
174 } | |
175 else if(token(s) == '%') { | |
176 length = va_arg(*ap, size_t); | |
177 } | |
178 else { | |
179 prev_token(s); | |
180 length = strlen(str); | |
181 } | |
182 | |
183 if(strbuffer_append_bytes(&strbuff, str, length) == -1) { | |
184 set_error(s, "<internal>", "Out of memory"); | |
185 strbuffer_close(&strbuff); | |
186 return NULL; | |
187 } | |
188 | |
189 next_token(s); | |
190 if(token(s) != '+') { | |
191 prev_token(s); | |
192 break; | |
193 } | |
194 } | |
195 | |
196 if(!utf8_check_string(strbuff.value, strbuff.length)) { | |
197 set_error(s, "<args>", "Invalid UTF-8 %s", purpose); | |
198 strbuffer_close(&strbuff); | |
199 return NULL; | |
200 } | |
201 | |
202 *out_len = strbuff.length; | |
203 *ours = 1; | |
204 return strbuffer_steal_value(&strbuff); | |
205 } | |
206 | |
207 static json_t *pack_object(scanner_t *s, va_list *ap) | |
208 { | |
209 json_t *object = json_object(); | |
210 next_token(s); | |
211 | |
212 while(token(s) != '}') { | |
213 char *key; | |
214 size_t len; | |
215 int ours; | |
216 json_t *value; | |
217 | |
218 if(!token(s)) { | |
219 set_error(s, "<format>", "Unexpected end of format string"); | |
220 goto error; | |
221 } | |
222 | |
223 if(token(s) != 's') { | |
224 set_error(s, "<format>", "Expected format 's', got '%c'", token(s)); | |
225 goto error; | |
226 } | |
227 | |
228 key = read_string(s, ap, "object key", &len, &ours); | |
229 if(!key) | |
230 goto error; | |
231 | |
232 next_token(s); | |
233 | |
234 value = pack(s, ap); | |
235 if(!value) { | |
236 if(ours) | |
237 jsonp_free(key); | |
238 | |
239 goto error; | |
240 } | |
241 | |
242 if(json_object_set_new_nocheck(object, key, value)) { | |
243 if(ours) | |
244 jsonp_free(key); | |
245 | |
246 set_error(s, "<internal>", "Unable to add key \"%s\"", key); | |
247 goto error; | |
248 } | |
249 | |
250 if(ours) | |
251 jsonp_free(key); | |
252 | |
253 next_token(s); | |
254 } | |
255 | |
256 return object; | |
257 | |
258 error: | |
259 json_decref(object); | |
260 return NULL; | |
261 } | |
262 | |
263 static json_t *pack_array(scanner_t *s, va_list *ap) | |
264 { | |
265 json_t *array = json_array(); | |
266 next_token(s); | |
267 | |
268 while(token(s) != ']') { | |
269 json_t *value; | |
270 | |
271 if(!token(s)) { | |
272 set_error(s, "<format>", "Unexpected end of format string"); | |
273 goto error; | |
274 } | |
275 | |
276 value = pack(s, ap); | |
277 if(!value) | |
278 goto error; | |
279 | |
280 if(json_array_append_new(array, value)) { | |
281 set_error(s, "<internal>", "Unable to append to array"); | |
282 goto error; | |
283 } | |
284 | |
285 next_token(s); | |
286 } | |
287 return array; | |
288 | |
289 error: | |
290 json_decref(array); | |
291 return NULL; | |
292 } | |
293 | |
294 static json_t *pack(scanner_t *s, va_list *ap) | |
295 { | |
296 switch(token(s)) { | |
297 case '{': | |
298 return pack_object(s, ap); | |
299 | |
300 case '[': | |
301 return pack_array(s, ap); | |
302 | |
303 case 's': /* string */ | |
304 { | |
305 char *str; | |
306 size_t len; | |
307 int ours; | |
308 | |
309 str = read_string(s, ap, "string", &len, &ours); | |
310 if(!str) | |
311 return NULL; | |
312 | |
313 if (ours) | |
314 return jsonp_stringn_nocheck_own(str, len); | |
315 else | |
316 return json_stringn_nocheck(str, len); | |
317 } | |
318 | |
319 case 'n': /* null */ | |
320 return json_null(); | |
321 | |
322 case 'b': /* boolean */ | |
323 return va_arg(*ap, int) ? json_true() : json_false(); | |
324 | |
325 case 'i': /* integer from int */ | |
326 return json_integer(va_arg(*ap, int)); | |
327 | |
328 case 'I': /* integer from json_int_t */ | |
329 return json_integer(va_arg(*ap, json_int_t)); | |
330 | |
331 case 'f': /* real */ | |
332 return json_real(va_arg(*ap, double)); | |
333 | |
334 case 'O': /* a json_t object; increments refcount */ | |
335 return json_incref(va_arg(*ap, json_t *)); | |
336 | |
337 case 'o': /* a json_t object; doesn't increment refcount */ | |
338 return va_arg(*ap, json_t *); | |
339 | |
340 default: | |
341 set_error(s, "<format>", "Unexpected format character '%c'", | |
342 token(s)); | |
343 return NULL; | |
344 } | |
345 } | |
346 | |
347 static int unpack(scanner_t *s, json_t *root, va_list *ap); | |
348 | |
349 static int unpack_object(scanner_t *s, json_t *root, va_list *ap) | |
350 { | |
351 int ret = -1; | |
352 int strict = 0; | |
353 int gotopt = 0; | |
354 | |
355 /* Use a set (emulated by a hashtable) to check that all object | |
356 keys are accessed. Checking that the correct number of keys | |
357 were accessed is not enough, as the same key can be unpacked | |
358 multiple times. | |
359 */ | |
360 hashtable_t key_set; | |
361 | |
362 if(hashtable_init(&key_set)) { | |
363 set_error(s, "<internal>", "Out of memory"); | |
364 return -1; | |
365 } | |
366 | |
367 if(root && !json_is_object(root)) { | |
368 set_error(s, "<validation>", "Expected object, got %s", | |
369 type_name(root)); | |
370 goto out; | |
371 } | |
372 next_token(s); | |
373 | |
374 while(token(s) != '}') { | |
375 const char *key; | |
376 json_t *value; | |
377 int opt = 0; | |
378 | |
379 if(strict != 0) { | |
380 set_error(s, "<format>", "Expected '}' after '%c', got '%c'", | |
381 (strict == 1 ? '!' : '*'), token(s)); | |
382 goto out; | |
383 } | |
384 | |
385 if(!token(s)) { | |
386 set_error(s, "<format>", "Unexpected end of format string"); | |
387 goto out; | |
388 } | |
389 | |
390 if(token(s) == '!' || token(s) == '*') { | |
391 strict = (token(s) == '!' ? 1 : -1); | |
392 next_token(s); | |
393 continue; | |
394 } | |
395 | |
396 if(token(s) != 's') { | |
397 set_error(s, "<format>", "Expected format 's', got '%c'", token(s)); | |
398 goto out; | |
399 } | |
400 | |
401 key = va_arg(*ap, const char *); | |
402 if(!key) { | |
403 set_error(s, "<args>", "NULL object key"); | |
404 goto out; | |
405 } | |
406 | |
407 next_token(s); | |
408 | |
409 if(token(s) == '?') { | |
410 opt = gotopt = 1; | |
411 next_token(s); | |
412 } | |
413 | |
414 if(!root) { | |
415 /* skipping */ | |
416 value = NULL; | |
417 } | |
418 else { | |
419 value = json_object_get(root, key); | |
420 if(!value && !opt) { | |
421 set_error(s, "<validation>", "Object item not found: %s", key); | |
422 goto out; | |
423 } | |
424 } | |
425 | |
426 if(unpack(s, value, ap)) | |
427 goto out; | |
428 | |
429 hashtable_set(&key_set, key, 0, json_null()); | |
430 next_token(s); | |
431 } | |
432 | |
433 if(strict == 0 && (s->flags & JSON_STRICT)) | |
434 strict = 1; | |
435 | |
436 if(root && strict == 1) { | |
437 /* We need to check that all non optional items have been parsed */ | |
438 const char *key; | |
439 json_t *value; | |
440 long unpacked = 0; | |
441 if (gotopt) { | |
442 /* We have optional keys, we need to iter on each key */ | |
443 json_object_foreach(root, key, value) { | |
444 if(!hashtable_get(&key_set, key)) { | |
445 unpacked++; | |
446 } | |
447 } | |
448 } else { | |
449 /* No optional keys, we can just compare the number of items */ | |
450 unpacked = (long)json_object_size(root) - (long)key_set.size; | |
451 } | |
452 if (unpacked) { | |
453 set_error(s, "<validation>", "%li object item(s) left unpacked", unpacked); | |
454 goto out; | |
455 } | |
456 } | |
457 | |
458 ret = 0; | |
459 | |
460 out: | |
461 hashtable_close(&key_set); | |
462 return ret; | |
463 } | |
464 | |
465 static int unpack_array(scanner_t *s, json_t *root, va_list *ap) | |
466 { | |
467 size_t i = 0; | |
468 int strict = 0; | |
469 | |
470 if(root && !json_is_array(root)) { | |
471 set_error(s, "<validation>", "Expected array, got %s", type_name(root)); | |
472 return -1; | |
473 } | |
474 next_token(s); | |
475 | |
476 while(token(s) != ']') { | |
477 json_t *value; | |
478 | |
479 if(strict != 0) { | |
480 set_error(s, "<format>", "Expected ']' after '%c', got '%c'", | |
481 (strict == 1 ? '!' : '*'), | |
482 token(s)); | |
483 return -1; | |
484 } | |
485 | |
486 if(!token(s)) { | |
487 set_error(s, "<format>", "Unexpected end of format string"); | |
488 return -1; | |
489 } | |
490 | |
491 if(token(s) == '!' || token(s) == '*') { | |
492 strict = (token(s) == '!' ? 1 : -1); | |
493 next_token(s); | |
494 continue; | |
495 } | |
496 | |
497 if(!strchr(unpack_value_starters, token(s))) { | |
498 set_error(s, "<format>", "Unexpected format character '%c'", | |
499 token(s)); | |
500 return -1; | |
501 } | |
502 | |
503 if(!root) { | |
504 /* skipping */ | |
505 value = NULL; | |
506 } | |
507 else { | |
508 value = json_array_get(root, i); | |
509 if(!value) { | |
510 set_error(s, "<validation>", "Array index %lu out of range", | |
511 (unsigned long)i); | |
512 return -1; | |
513 } | |
514 } | |
515 | |
516 if(unpack(s, value, ap)) | |
517 return -1; | |
518 | |
519 next_token(s); | |
520 i++; | |
521 } | |
522 | |
523 if(strict == 0 && (s->flags & JSON_STRICT)) | |
524 strict = 1; | |
525 | |
526 if(root && strict == 1 && i != json_array_size(root)) { | |
527 long diff = (long)json_array_size(root) - (long)i; | |
528 set_error(s, "<validation>", "%li array item(s) left unpacked", diff); | |
529 return -1; | |
530 } | |
531 | |
532 return 0; | |
533 } | |
534 | |
535 static int unpack(scanner_t *s, json_t *root, va_list *ap) | |
536 { | |
537 switch(token(s)) | |
538 { | |
539 case '{': | |
540 return unpack_object(s, root, ap); | |
541 | |
542 case '[': | |
543 return unpack_array(s, root, ap); | |
544 | |
545 case 's': | |
546 if(root && !json_is_string(root)) { | |
547 set_error(s, "<validation>", "Expected string, got %s", | |
548 type_name(root)); | |
549 return -1; | |
550 } | |
551 | |
552 if(!(s->flags & JSON_VALIDATE_ONLY)) { | |
553 const char **str_target; | |
554 size_t *len_target = NULL; | |
555 | |
556 str_target = va_arg(*ap, const char **); | |
557 if(!str_target) { | |
558 set_error(s, "<args>", "NULL string argument"); | |
559 return -1; | |
560 } | |
561 | |
562 next_token(s); | |
563 | |
564 if(token(s) == '%') { | |
565 len_target = va_arg(*ap, size_t *); | |
566 if(!len_target) { | |
567 set_error(s, "<args>", "NULL string length argument"); | |
568 return -1; | |
569 } | |
570 } | |
571 else | |
572 prev_token(s); | |
573 | |
574 if(root) { | |
575 *str_target = json_string_value(root); | |
576 if(len_target) | |
577 *len_target = json_string_length(root); | |
578 } | |
579 } | |
580 return 0; | |
581 | |
582 case 'i': | |
583 if(root && !json_is_integer(root)) { | |
584 set_error(s, "<validation>", "Expected integer, got %s", | |
585 type_name(root)); | |
586 return -1; | |
587 } | |
588 | |
589 if(!(s->flags & JSON_VALIDATE_ONLY)) { | |
590 int *target = va_arg(*ap, int*); | |
591 if(root) | |
592 *target = (int)json_integer_value(root); | |
593 } | |
594 | |
595 return 0; | |
596 | |
597 case 'I': | |
598 if(root && !json_is_integer(root)) { | |
599 set_error(s, "<validation>", "Expected integer, got %s", | |
600 type_name(root)); | |
601 return -1; | |
602 } | |
603 | |
604 if(!(s->flags & JSON_VALIDATE_ONLY)) { | |
605 json_int_t *target = va_arg(*ap, json_int_t*); | |
606 if(root) | |
607 *target = json_integer_value(root); | |
608 } | |
609 | |
610 return 0; | |
611 | |
612 case 'b': | |
613 if(root && !json_is_boolean(root)) { | |
614 set_error(s, "<validation>", "Expected true or false, got %s", | |
615 type_name(root)); | |
616 return -1; | |
617 } | |
618 | |
619 if(!(s->flags & JSON_VALIDATE_ONLY)) { | |
620 int *target = va_arg(*ap, int*); | |
621 if(root) | |
622 *target = json_is_true(root); | |
623 } | |
624 | |
625 return 0; | |
626 | |
627 case 'f': | |
628 if(root && !json_is_real(root)) { | |
629 set_error(s, "<validation>", "Expected real, got %s", | |
630 type_name(root)); | |
631 return -1; | |
632 } | |
633 | |
634 if(!(s->flags & JSON_VALIDATE_ONLY)) { | |
635 double *target = va_arg(*ap, double*); | |
636 if(root) | |
637 *target = json_real_value(root); | |
638 } | |
639 | |
640 return 0; | |
641 | |
642 case 'F': | |
643 if(root && !json_is_number(root)) { | |
644 set_error(s, "<validation>", "Expected real or integer, got %s", | |
645 type_name(root)); | |
646 return -1; | |
647 } | |
648 | |
649 if(!(s->flags & JSON_VALIDATE_ONLY)) { | |
650 double *target = va_arg(*ap, double*); | |
651 if(root) | |
652 *target = json_number_value(root); | |
653 } | |
654 | |
655 return 0; | |
656 | |
657 case 'O': | |
658 if(root && !(s->flags & JSON_VALIDATE_ONLY)) | |
659 json_incref(root); | |
660 /* Fall through */ | |
661 | |
662 case 'o': | |
663 if(!(s->flags & JSON_VALIDATE_ONLY)) { | |
664 json_t **target = va_arg(*ap, json_t**); | |
665 if(root) | |
666 *target = root; | |
667 } | |
668 | |
669 return 0; | |
670 | |
671 case 'n': | |
672 /* Never assign, just validate */ | |
673 if(root && !json_is_null(root)) { | |
674 set_error(s, "<validation>", "Expected null, got %s", | |
675 type_name(root)); | |
676 return -1; | |
677 } | |
678 return 0; | |
679 | |
680 default: | |
681 set_error(s, "<format>", "Unexpected format character '%c'", | |
682 token(s)); | |
683 return -1; | |
684 } | |
685 } | |
686 | |
687 json_t *json_vpack_ex(json_error_t *error, size_t flags, | |
688 const char *fmt, va_list ap) | |
689 { | |
690 scanner_t s; | |
691 va_list ap_copy; | |
692 json_t *value; | |
693 | |
694 if(!fmt || !*fmt) { | |
695 jsonp_error_init(error, "<format>"); | |
696 jsonp_error_set(error, -1, -1, 0, "NULL or empty format string"); | |
697 return NULL; | |
698 } | |
699 jsonp_error_init(error, NULL); | |
700 | |
701 scanner_init(&s, error, flags, fmt); | |
702 next_token(&s); | |
703 | |
704 va_copy(ap_copy, ap); | |
705 value = pack(&s, &ap_copy); | |
706 va_end(ap_copy); | |
707 | |
708 if(!value) | |
709 return NULL; | |
710 | |
711 next_token(&s); | |
712 if(token(&s)) { | |
713 json_decref(value); | |
714 set_error(&s, "<format>", "Garbage after format string"); | |
715 return NULL; | |
716 } | |
717 | |
718 return value; | |
719 } | |
720 | |
721 json_t *json_pack_ex(json_error_t *error, size_t flags, const char *fmt, ...) | |
722 { | |
723 json_t *value; | |
724 va_list ap; | |
725 | |
726 va_start(ap, fmt); | |
727 value = json_vpack_ex(error, flags, fmt, ap); | |
728 va_end(ap); | |
729 | |
730 return value; | |
731 } | |
732 | |
733 json_t *json_pack(const char *fmt, ...) | |
734 { | |
735 json_t *value; | |
736 va_list ap; | |
737 | |
738 va_start(ap, fmt); | |
739 value = json_vpack_ex(NULL, 0, fmt, ap); | |
740 va_end(ap); | |
741 | |
742 return value; | |
743 } | |
744 | |
745 int json_vunpack_ex(json_t *root, json_error_t *error, size_t flags, | |
746 const char *fmt, va_list ap) | |
747 { | |
748 scanner_t s; | |
749 va_list ap_copy; | |
750 | |
751 if(!root) { | |
752 jsonp_error_init(error, "<root>"); | |
753 jsonp_error_set(error, -1, -1, 0, "NULL root value"); | |
754 return -1; | |
755 } | |
756 | |
757 if(!fmt || !*fmt) { | |
758 jsonp_error_init(error, "<format>"); | |
759 jsonp_error_set(error, -1, -1, 0, "NULL or empty format string"); | |
760 return -1; | |
761 } | |
762 jsonp_error_init(error, NULL); | |
763 | |
764 scanner_init(&s, error, flags, fmt); | |
765 next_token(&s); | |
766 | |
767 va_copy(ap_copy, ap); | |
768 if(unpack(&s, root, &ap_copy)) { | |
769 va_end(ap_copy); | |
770 return -1; | |
771 } | |
772 va_end(ap_copy); | |
773 | |
774 next_token(&s); | |
775 if(token(&s)) { | |
776 set_error(&s, "<format>", "Garbage after format string"); | |
777 return -1; | |
778 } | |
779 | |
780 return 0; | |
781 } | |
782 | |
783 int json_unpack_ex(json_t *root, json_error_t *error, size_t flags, const char *fmt, ...) | |
784 { | |
785 int ret; | |
786 va_list ap; | |
787 | |
788 va_start(ap, fmt); | |
789 ret = json_vunpack_ex(root, error, flags, fmt, ap); | |
790 va_end(ap); | |
791 | |
792 return ret; | |
793 } | |
794 | |
795 int json_unpack(json_t *root, const char *fmt, ...) | |
796 { | |
797 int ret; | |
798 va_list ap; | |
799 | |
800 va_start(ap, fmt); | |
801 ret = json_vunpack_ex(root, NULL, 0, fmt, ap); | |
802 va_end(ap); | |
803 | |
804 return ret; | |
805 } |