Skip to content

Commit 3edac28

Browse files
calilkhalilnikias
authored andcommitted
Add JSON coercion support for non-JSON plist types
- Add PLIST_OPT_COERCE option to coerce PLIST_DATE, PLIST_DATA, and PLIST_UID to JSON-compatible types (ISO 8601 strings, Base64 strings, and integers) - Add plist_to_json_with_options() function to allow passing coercion options (and others) - Update plist_write_to_string() and plist_write_to_stream() to support coercion option - Add --coerce flag to plistutil for JSON output - Create plist2json symlink that automatically enables coercion when invoked
1 parent 6e03a1d commit 3edac28

5 files changed

Lines changed: 165 additions & 31 deletions

File tree

include/plist/plist.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ extern "C"
175175
PLIST_OPT_PARTIAL_DATA = 1 << 1, /**< Print 24 bytes maximum of #PLIST_DATA values. If the data is longer than 24 bytes, the first 16 and last 8 bytes will be written. Only valid for #PLIST_FORMAT_PRINT. */
176176
PLIST_OPT_NO_NEWLINE = 1 << 2, /**< Do not print a final newline character. Only valid for #PLIST_FORMAT_PRINT, #PLIST_FORMAT_LIMD, and #PLIST_FORMAT_PLUTIL. */
177177
PLIST_OPT_INDENT = 1 << 3, /**< Indent each line of output. Currently only #PLIST_FORMAT_PRINT and #PLIST_FORMAT_LIMD are supported. Use #PLIST_OPT_INDENT_BY() macro to specify the level of indentation. */
178+
PLIST_OPT_COERCE = 1 << 4, /**< Coerce plist types that have no native JSON representation into JSON-compatible types.
179+
#PLIST_DATE is converted to an ISO 8601 date string,
180+
#PLIST_DATA is converted to a Base64-encoded string, and
181+
#PLIST_UID is converted to an integer.
182+
Only valid for #PLIST_FORMAT_JSON. Without this option, these types cause #PLIST_ERR_FORMAT. */
178183
} plist_write_options_t;
179184

180185
/** To be used with #PLIST_OPT_INDENT - encodes the level of indentation for OR'ing it into the #plist_write_options_t bitfield. */
@@ -937,6 +942,29 @@ extern "C"
937942
*/
938943
PLIST_API plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, int prettify);
939944

945+
/**
946+
* Export the #plist_t structure to JSON format with extended options.
947+
*
948+
* When \a PLIST_OPT_COMPACT is set in \a options, the resulting JSON
949+
* will be non-prettified.
950+
*
951+
* When \a PLIST_OPT_COERCE is set in \a options, plist types that have
952+
* no native JSON representation are converted to JSON-compatible types
953+
* instead of returning #PLIST_ERR_FORMAT:
954+
* - #PLIST_DATE is serialized as an ISO 8601 date string
955+
* - #PLIST_DATA is serialized as a Base64-encoded string
956+
* - #PLIST_UID is serialized as an integer
957+
*
958+
* @param plist the root node to export
959+
* @param plist_json a pointer to a char* buffer. This function allocates the memory,
960+
* caller is responsible for freeing it.
961+
* @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
962+
* @param options One or more bitwise ORed values of #plist_write_options_t.
963+
* @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
964+
* @note Use plist_mem_free() to free the allocated memory.
965+
*/
966+
PLIST_API plist_err_t plist_to_json_with_options(plist_t plist, char **plist_json, uint32_t* length, plist_write_options_t options);
967+
940968
/**
941969
* Export the #plist_t structure to OpenStep format.
942970
*

src/jplist.c

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
#include "strbuf.h"
4040
#include "jsmn.h"
4141
#include "hashtable.h"
42+
#include "base64.h"
43+
#include "time64.h"
44+
45+
#define MAC_EPOCH 978307200
4246

4347
#ifdef DEBUG
4448
static int plist_json_debug = 0;
@@ -115,7 +119,7 @@ static size_t dtostr(char *buf, size_t bufsize, double realval)
115119
return len;
116120
}
117121

118-
static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t depth, int prettify)
122+
static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t depth, int prettify, int coerce)
119123
{
120124
plist_data_t node_data = NULL;
121125

@@ -211,7 +215,7 @@ static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t dept
211215
str_buf_append(*outbuf, " ", 2);
212216
}
213217
}
214-
plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify);
218+
plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify, coerce);
215219
if (res < 0) {
216220
return res;
217221
}
@@ -239,7 +243,7 @@ static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t dept
239243
str_buf_append(*outbuf, " ", 2);
240244
}
241245
}
242-
plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify);
246+
plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify, coerce);
243247
if (res < 0) {
244248
return res;
245249
}
@@ -260,17 +264,60 @@ static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t dept
260264
str_buf_append(*outbuf, "}", 1);
261265
} break;
262266
case PLIST_DATA:
263-
// NOT VALID FOR JSON
264-
PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n");
265-
return PLIST_ERR_FORMAT;
267+
if (coerce) {
268+
size_t b64_len = ((node_data->length + 2) / 3) * 4;
269+
char *b64_buf = (char*)malloc(b64_len + 1);
270+
if (!b64_buf) {
271+
return PLIST_ERR_NO_MEM;
272+
}
273+
size_t actual_len = base64encode(b64_buf, node_data->buff, node_data->length);
274+
str_buf_append(*outbuf, "\"", 1);
275+
str_buf_append(*outbuf, b64_buf, actual_len);
276+
str_buf_append(*outbuf, "\"", 1);
277+
free(b64_buf);
278+
} else {
279+
PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n");
280+
return PLIST_ERR_FORMAT;
281+
}
282+
break;
266283
case PLIST_DATE:
267-
// NOT VALID FOR JSON
268-
PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n");
269-
return PLIST_ERR_FORMAT;
284+
if (coerce) {
285+
Time64_T timev = (Time64_T)node_data->realval + MAC_EPOCH;
286+
struct TM _btime;
287+
struct TM *btime = gmtime64_r(&timev, &_btime);
288+
char datebuf[32];
289+
size_t datelen = 0;
290+
if (btime) {
291+
struct tm _tmcopy;
292+
copy_TM64_to_tm(btime, &_tmcopy);
293+
datelen = strftime(datebuf, sizeof(datebuf), "%Y-%m-%dT%H:%M:%SZ", &_tmcopy);
294+
}
295+
if (datelen <= 0) {
296+
datelen = snprintf(datebuf, sizeof(datebuf), "1970-01-01T00:00:00Z");
297+
}
298+
str_buf_append(*outbuf, "\"", 1);
299+
str_buf_append(*outbuf, datebuf, datelen);
300+
str_buf_append(*outbuf, "\"", 1);
301+
} else {
302+
PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n");
303+
return PLIST_ERR_FORMAT;
304+
}
305+
break;
270306
case PLIST_UID:
271-
// NOT VALID FOR JSON
272-
PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n");
273-
return PLIST_ERR_FORMAT;
307+
if (coerce) {
308+
val = (char*)malloc(64);
309+
if (node_data->length == 16) {
310+
val_len = snprintf(val, 64, "%" PRIu64, node_data->intval);
311+
} else {
312+
val_len = snprintf(val, 64, "%" PRIi64, node_data->intval);
313+
}
314+
str_buf_append(*outbuf, val, val_len);
315+
free(val);
316+
} else {
317+
PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n");
318+
return PLIST_ERR_FORMAT;
319+
}
320+
break;
274321
default:
275322
return PLIST_ERR_UNKNOWN;
276323
}
@@ -316,7 +363,7 @@ static int num_digits_u(uint64_t i)
316363
return n;
317364
}
318365

319-
static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify, hashtable_t *visited)
366+
static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify, int coerce, hashtable_t *visited)
320367
{
321368
plist_data_t data;
322369
if (!node) {
@@ -341,7 +388,7 @@ static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t dep
341388
node_t ch;
342389
unsigned int n_children = node_n_children(node);
343390
for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) {
344-
plist_err_t res = _node_estimate_size(ch, size, depth + 1, prettify, visited);
391+
plist_err_t res = _node_estimate_size(ch, size, depth + 1, prettify, coerce, visited);
345392
if (res < 0) {
346393
return res;
347394
}
@@ -398,17 +445,36 @@ static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t dep
398445
*size += 2;
399446
break;
400447
case PLIST_DATA:
401-
// NOT VALID FOR JSON
402-
PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n");
403-
return PLIST_ERR_FORMAT;
448+
if (coerce) {
449+
// base64 encoded string: 2 quotes + ((len+2)/3)*4 base64 chars
450+
*size += 2 + ((data->length + 2) / 3) * 4;
451+
} else {
452+
PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n");
453+
return PLIST_ERR_FORMAT;
454+
}
455+
break;
404456
case PLIST_DATE:
405-
// NOT VALID FOR JSON
406-
PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n");
407-
return PLIST_ERR_FORMAT;
457+
if (coerce) {
458+
// ISO 8601 string: "YYYY-MM-DDTHH:MM:SSZ" = 22 chars max
459+
*size += 24;
460+
} else {
461+
PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n");
462+
return PLIST_ERR_FORMAT;
463+
}
464+
break;
408465
case PLIST_UID:
409-
// NOT VALID FOR JSON
410-
PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n");
411-
return PLIST_ERR_FORMAT;
466+
if (coerce) {
467+
// integer representation
468+
if (data->length == 16) {
469+
*size += num_digits_u(data->intval);
470+
} else {
471+
*size += num_digits_i((int64_t)data->intval);
472+
}
473+
} else {
474+
PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n");
475+
return PLIST_ERR_FORMAT;
476+
}
477+
break;
412478
default:
413479
PLIST_JSON_WRITE_ERR("invalid node type encountered\n");
414480
return PLIST_ERR_UNKNOWN;
@@ -417,16 +483,22 @@ static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t dep
417483
return PLIST_ERR_SUCCESS;
418484
}
419485

420-
static plist_err_t node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify)
486+
static plist_err_t node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify, int coerce)
421487
{
422488
hashtable_t *visited = hash_table_new(plist_node_ptr_hash, plist_node_ptr_compare, NULL);
423489
if (!visited) return PLIST_ERR_NO_MEM;
424-
plist_err_t err = _node_estimate_size(node, size, depth, prettify, visited);
490+
plist_err_t err = _node_estimate_size(node, size, depth, prettify, coerce, visited);
425491
hash_table_destroy(visited);
426492
return err;
427493
}
428494

429495
plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, int prettify)
496+
{
497+
plist_write_options_t opts = prettify ? PLIST_OPT_NONE : PLIST_OPT_COMPACT;
498+
return plist_to_json_with_options(plist, plist_json, length, opts);
499+
}
500+
501+
plist_err_t plist_to_json_with_options(plist_t plist, char **plist_json, uint32_t* length, plist_write_options_t options)
430502
{
431503
uint64_t size = 0;
432504
plist_err_t res;
@@ -440,7 +512,10 @@ plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, in
440512
return PLIST_ERR_FORMAT;
441513
}
442514

443-
res = node_estimate_size((node_t)plist, &size, 0, prettify);
515+
int prettify = !(options & PLIST_OPT_COMPACT);
516+
int coerce = options & PLIST_OPT_COERCE;
517+
518+
res = node_estimate_size((node_t)plist, &size, 0, prettify, coerce);
444519
if (res < 0) {
445520
return res;
446521
}
@@ -451,7 +526,7 @@ plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, in
451526
return PLIST_ERR_NO_MEM;
452527
}
453528

454-
res = node_to_json((node_t)plist, &outbuf, 0, prettify);
529+
res = node_to_json((node_t)plist, &outbuf, 0, prettify, coerce);
455530
if (res < 0) {
456531
str_buf_free(outbuf);
457532
*plist_json = NULL;

src/plist.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2404,7 +2404,7 @@ plist_err_t plist_write_to_string(plist_t plist, char **output, uint32_t* length
24042404
err = plist_to_xml(plist, output, length);
24052405
break;
24062406
case PLIST_FORMAT_JSON:
2407-
err = plist_to_json(plist, output, length, ((options & PLIST_OPT_COMPACT) == 0));
2407+
err = plist_to_json_with_options(plist, output, length, options);
24082408
break;
24092409
case PLIST_FORMAT_OSTEP:
24102410
err = plist_to_openstep(plist, output, length, ((options & PLIST_OPT_COMPACT) == 0));
@@ -2442,7 +2442,7 @@ plist_err_t plist_write_to_stream(plist_t plist, FILE *stream, plist_format_t fo
24422442
err = plist_to_xml(plist, &output, &length);
24432443
break;
24442444
case PLIST_FORMAT_JSON:
2445-
err = plist_to_json(plist, &output, &length, ((options & PLIST_OPT_COMPACT) == 0));
2445+
err = plist_to_json_with_options(plist, &output, &length, options);
24462446
break;
24472447
case PLIST_FORMAT_OSTEP:
24482448
err = plist_to_openstep(plist, &output, &length, ((options & PLIST_OPT_COMPACT) == 0));

tools/Makefile.am

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@ bin_PROGRAMS = plistutil
1111
plistutil_SOURCES = plistutil.c
1212
plistutil_LDADD = $(top_builddir)/src/libplist-2.0.la
1313

14+
install-exec-hook:
15+
cd $(DESTDIR)$(bindir) && ln -sf plistutil plist2json
16+
17+
uninstall-hook:
18+
rm -f $(DESTDIR)$(bindir)/plist2json
19+
1420
endif

tools/plistutil.c

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ typedef struct _options
5252
#define OPT_DEBUG (1 << 0)
5353
#define OPT_COMPACT (1 << 1)
5454
#define OPT_SORT (1 << 2)
55+
#define OPT_COERCE (1 << 3)
5556

5657
static void print_usage(int argc, char *argv[])
5758
{
@@ -74,6 +75,10 @@ static void print_usage(int argc, char *argv[])
7475
printf(" -n, --nodepath PATH Restrict output to nodepath defined by PATH.\n");
7576
printf(" -c, --compact JSON and OpenStep only: Print output in compact form.\n");
7677
printf(" By default, the output will be pretty-printed.\n");
78+
printf(" -C, --coerce JSON only: Coerce non-JSON plist types to JSON-compatible\n");
79+
printf(" representations. Date values become ISO 8601 strings,\n");
80+
printf(" data values become Base64-encoded strings, and UID values\n");
81+
printf(" become integers. Implied when invoked as plist2json.\n");
7782
printf(" -s, --sort Sort all dictionary nodes lexicographically by key\n");
7883
printf(" before converting to the output format.\n");
7984
printf(" -d, --debug Enable extended debug output\n");
@@ -96,6 +101,7 @@ static options_t *parse_arguments(int argc, char *argv[])
96101
{ "outfile", required_argument, 0, 'o' },
97102
{ "format", required_argument, 0, 'f' },
98103
{ "compact", no_argument, 0, 'c' },
104+
{ "coerce", no_argument, 0, 'C' },
99105
{ "sort", no_argument, 0, 's' },
100106
{ "print", required_argument, 0, 'p' },
101107
{ "nodepath", required_argument, 0, 'n' },
@@ -106,7 +112,7 @@ static options_t *parse_arguments(int argc, char *argv[])
106112
};
107113

108114
int c;
109-
while ((c = getopt_long(argc, argv, "i:o:f:csp:n:dhv", long_options, NULL)) != -1)
115+
while ((c = getopt_long(argc, argv, "i:o:f:cCsp:n:dhv", long_options, NULL)) != -1)
110116
{
111117
switch (c)
112118
{
@@ -154,6 +160,10 @@ static options_t *parse_arguments(int argc, char *argv[])
154160
options->flags |= OPT_COMPACT;
155161
break;
156162

163+
case 'C':
164+
options->flags |= OPT_COERCE;
165+
break;
166+
157167
case 's':
158168
options->flags |= OPT_SORT;
159169
break;
@@ -230,6 +240,18 @@ int main(int argc, char *argv[])
230240
return 0;
231241
}
232242

243+
// detect invocation as plist2json symlink
244+
{
245+
char *progname = strrchr(argv[0], '/');
246+
progname = progname ? progname + 1 : argv[0];
247+
if (!strcmp(progname, "plist2json")) {
248+
if (options->out_fmt == 0) {
249+
options->out_fmt = PLIST_FORMAT_JSON;
250+
}
251+
options->flags |= OPT_COERCE;
252+
}
253+
}
254+
233255
if (options->flags & OPT_DEBUG)
234256
{
235257
plist_set_debug(1);
@@ -404,7 +426,10 @@ int main(int argc, char *argv[])
404426
} else if (options->out_fmt == PLIST_FORMAT_XML) {
405427
output_res = plist_to_xml(root_node, &plist_out, &size);
406428
} else if (options->out_fmt == PLIST_FORMAT_JSON) {
407-
output_res = plist_to_json(root_node, &plist_out, &size, !(options->flags & OPT_COMPACT));
429+
plist_write_options_t wropts = PLIST_OPT_NONE;
430+
if (options->flags & OPT_COMPACT) wropts |= PLIST_OPT_COMPACT;
431+
if (options->flags & OPT_COERCE) wropts |= PLIST_OPT_COERCE;
432+
output_res = plist_to_json_with_options(root_node, &plist_out, &size, wropts);
408433
} else if (options->out_fmt == PLIST_FORMAT_OSTEP) {
409434
output_res = plist_to_openstep(root_node, &plist_out, &size, !(options->flags & OPT_COMPACT));
410435
} else {

0 commit comments

Comments
 (0)