From 3edac28498d883f1f768699ee15ce85a82bb2a7b Mon Sep 17 00:00:00 2001 From: Calil Khalil Date: Sat, 21 Feb 2026 10:39:24 -0300 Subject: 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 --- include/plist/plist.h | 28 +++++++++++ src/jplist.c | 129 +++++++++++++++++++++++++++++++++++++++----------- src/plist.c | 4 +- tools/Makefile.am | 6 +++ tools/plistutil.c | 29 +++++++++++- 5 files changed, 165 insertions(+), 31 deletions(-) diff --git a/include/plist/plist.h b/include/plist/plist.h index b46b9a9..bd35c53 100644 --- a/include/plist/plist.h +++ b/include/plist/plist.h @@ -175,6 +175,11 @@ extern "C" 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. */ 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. */ 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. */ + PLIST_OPT_COERCE = 1 << 4, /**< Coerce plist types that have no native JSON representation into JSON-compatible types. + #PLIST_DATE is converted to an ISO 8601 date string, + #PLIST_DATA is converted to a Base64-encoded string, and + #PLIST_UID is converted to an integer. + Only valid for #PLIST_FORMAT_JSON. Without this option, these types cause #PLIST_ERR_FORMAT. */ } plist_write_options_t; /** 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" */ PLIST_API plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, int prettify); + /** + * Export the #plist_t structure to JSON format with extended options. + * + * When \a PLIST_OPT_COMPACT is set in \a options, the resulting JSON + * will be non-prettified. + * + * When \a PLIST_OPT_COERCE is set in \a options, plist types that have + * no native JSON representation are converted to JSON-compatible types + * instead of returning #PLIST_ERR_FORMAT: + * - #PLIST_DATE is serialized as an ISO 8601 date string + * - #PLIST_DATA is serialized as a Base64-encoded string + * - #PLIST_UID is serialized as an integer + * + * @param plist the root node to export + * @param plist_json a pointer to a char* buffer. This function allocates the memory, + * caller is responsible for freeing it. + * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer. + * @param options One or more bitwise ORed values of #plist_write_options_t. + * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure + * @note Use plist_mem_free() to free the allocated memory. + */ + PLIST_API plist_err_t plist_to_json_with_options(plist_t plist, char **plist_json, uint32_t* length, plist_write_options_t options); + /** * Export the #plist_t structure to OpenStep format. * diff --git a/src/jplist.c b/src/jplist.c index 0ac1e0b..410d4b3 100644 --- a/src/jplist.c +++ b/src/jplist.c @@ -39,6 +39,10 @@ #include "strbuf.h" #include "jsmn.h" #include "hashtable.h" +#include "base64.h" +#include "time64.h" + +#define MAC_EPOCH 978307200 #ifdef DEBUG static int plist_json_debug = 0; @@ -115,7 +119,7 @@ static size_t dtostr(char *buf, size_t bufsize, double realval) return len; } -static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t depth, int prettify) +static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t depth, int prettify, int coerce) { plist_data_t node_data = NULL; @@ -211,7 +215,7 @@ static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t dept str_buf_append(*outbuf, " ", 2); } } - plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify); + plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify, coerce); if (res < 0) { return res; } @@ -239,7 +243,7 @@ static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t dept str_buf_append(*outbuf, " ", 2); } } - plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify); + plist_err_t res = node_to_json(ch, outbuf, depth+1, prettify, coerce); if (res < 0) { return res; } @@ -260,17 +264,60 @@ static plist_err_t node_to_json(node_t node, bytearray_t **outbuf, uint32_t dept str_buf_append(*outbuf, "}", 1); } break; case PLIST_DATA: - // NOT VALID FOR JSON - PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n"); - return PLIST_ERR_FORMAT; + if (coerce) { + size_t b64_len = ((node_data->length + 2) / 3) * 4; + char *b64_buf = (char*)malloc(b64_len + 1); + if (!b64_buf) { + return PLIST_ERR_NO_MEM; + } + size_t actual_len = base64encode(b64_buf, node_data->buff, node_data->length); + str_buf_append(*outbuf, "\"", 1); + str_buf_append(*outbuf, b64_buf, actual_len); + str_buf_append(*outbuf, "\"", 1); + free(b64_buf); + } else { + PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n"); + return PLIST_ERR_FORMAT; + } + break; case PLIST_DATE: - // NOT VALID FOR JSON - PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n"); - return PLIST_ERR_FORMAT; + if (coerce) { + Time64_T timev = (Time64_T)node_data->realval + MAC_EPOCH; + struct TM _btime; + struct TM *btime = gmtime64_r(&timev, &_btime); + char datebuf[32]; + size_t datelen = 0; + if (btime) { + struct tm _tmcopy; + copy_TM64_to_tm(btime, &_tmcopy); + datelen = strftime(datebuf, sizeof(datebuf), "%Y-%m-%dT%H:%M:%SZ", &_tmcopy); + } + if (datelen <= 0) { + datelen = snprintf(datebuf, sizeof(datebuf), "1970-01-01T00:00:00Z"); + } + str_buf_append(*outbuf, "\"", 1); + str_buf_append(*outbuf, datebuf, datelen); + str_buf_append(*outbuf, "\"", 1); + } else { + PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n"); + return PLIST_ERR_FORMAT; + } + break; case PLIST_UID: - // NOT VALID FOR JSON - PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n"); - return PLIST_ERR_FORMAT; + if (coerce) { + val = (char*)malloc(64); + if (node_data->length == 16) { + val_len = snprintf(val, 64, "%" PRIu64, node_data->intval); + } else { + val_len = snprintf(val, 64, "%" PRIi64, node_data->intval); + } + str_buf_append(*outbuf, val, val_len); + free(val); + } else { + PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n"); + return PLIST_ERR_FORMAT; + } + break; default: return PLIST_ERR_UNKNOWN; } @@ -316,7 +363,7 @@ static int num_digits_u(uint64_t i) return n; } -static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify, hashtable_t *visited) +static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify, int coerce, hashtable_t *visited) { plist_data_t data; if (!node) { @@ -341,7 +388,7 @@ static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t dep node_t ch; unsigned int n_children = node_n_children(node); for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) { - plist_err_t res = _node_estimate_size(ch, size, depth + 1, prettify, visited); + plist_err_t res = _node_estimate_size(ch, size, depth + 1, prettify, coerce, visited); if (res < 0) { return res; } @@ -398,17 +445,36 @@ static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t dep *size += 2; break; case PLIST_DATA: - // NOT VALID FOR JSON - PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n"); - return PLIST_ERR_FORMAT; + if (coerce) { + // base64 encoded string: 2 quotes + ((len+2)/3)*4 base64 chars + *size += 2 + ((data->length + 2) / 3) * 4; + } else { + PLIST_JSON_WRITE_ERR("PLIST_DATA type is not valid for JSON format\n"); + return PLIST_ERR_FORMAT; + } + break; case PLIST_DATE: - // NOT VALID FOR JSON - PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n"); - return PLIST_ERR_FORMAT; + if (coerce) { + // ISO 8601 string: "YYYY-MM-DDTHH:MM:SSZ" = 22 chars max + *size += 24; + } else { + PLIST_JSON_WRITE_ERR("PLIST_DATE type is not valid for JSON format\n"); + return PLIST_ERR_FORMAT; + } + break; case PLIST_UID: - // NOT VALID FOR JSON - PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n"); - return PLIST_ERR_FORMAT; + if (coerce) { + // integer representation + if (data->length == 16) { + *size += num_digits_u(data->intval); + } else { + *size += num_digits_i((int64_t)data->intval); + } + } else { + PLIST_JSON_WRITE_ERR("PLIST_UID type is not valid for JSON format\n"); + return PLIST_ERR_FORMAT; + } + break; default: PLIST_JSON_WRITE_ERR("invalid node type encountered\n"); return PLIST_ERR_UNKNOWN; @@ -417,16 +483,22 @@ static plist_err_t _node_estimate_size(node_t node, uint64_t *size, uint32_t dep return PLIST_ERR_SUCCESS; } -static plist_err_t node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify) +static plist_err_t node_estimate_size(node_t node, uint64_t *size, uint32_t depth, int prettify, int coerce) { hashtable_t *visited = hash_table_new(plist_node_ptr_hash, plist_node_ptr_compare, NULL); if (!visited) return PLIST_ERR_NO_MEM; - plist_err_t err = _node_estimate_size(node, size, depth, prettify, visited); + plist_err_t err = _node_estimate_size(node, size, depth, prettify, coerce, visited); hash_table_destroy(visited); return err; } plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, int prettify) +{ + plist_write_options_t opts = prettify ? PLIST_OPT_NONE : PLIST_OPT_COMPACT; + return plist_to_json_with_options(plist, plist_json, length, opts); +} + +plist_err_t plist_to_json_with_options(plist_t plist, char **plist_json, uint32_t* length, plist_write_options_t options) { uint64_t size = 0; plist_err_t res; @@ -440,7 +512,10 @@ plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, in return PLIST_ERR_FORMAT; } - res = node_estimate_size((node_t)plist, &size, 0, prettify); + int prettify = !(options & PLIST_OPT_COMPACT); + int coerce = options & PLIST_OPT_COERCE; + + res = node_estimate_size((node_t)plist, &size, 0, prettify, coerce); if (res < 0) { return res; } @@ -451,7 +526,7 @@ plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, in return PLIST_ERR_NO_MEM; } - res = node_to_json((node_t)plist, &outbuf, 0, prettify); + res = node_to_json((node_t)plist, &outbuf, 0, prettify, coerce); if (res < 0) { str_buf_free(outbuf); *plist_json = NULL; diff --git a/src/plist.c b/src/plist.c index 7697a75..a6d3547 100644 --- a/src/plist.c +++ b/src/plist.c @@ -2404,7 +2404,7 @@ plist_err_t plist_write_to_string(plist_t plist, char **output, uint32_t* length err = plist_to_xml(plist, output, length); break; case PLIST_FORMAT_JSON: - err = plist_to_json(plist, output, length, ((options & PLIST_OPT_COMPACT) == 0)); + err = plist_to_json_with_options(plist, output, length, options); break; case PLIST_FORMAT_OSTEP: 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 err = plist_to_xml(plist, &output, &length); break; case PLIST_FORMAT_JSON: - err = plist_to_json(plist, &output, &length, ((options & PLIST_OPT_COMPACT) == 0)); + err = plist_to_json_with_options(plist, &output, &length, options); break; case PLIST_FORMAT_OSTEP: err = plist_to_openstep(plist, &output, &length, ((options & PLIST_OPT_COMPACT) == 0)); diff --git a/tools/Makefile.am b/tools/Makefile.am index 5883286..93b5b9f 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -11,4 +11,10 @@ bin_PROGRAMS = plistutil plistutil_SOURCES = plistutil.c plistutil_LDADD = $(top_builddir)/src/libplist-2.0.la +install-exec-hook: + cd $(DESTDIR)$(bindir) && ln -sf plistutil plist2json + +uninstall-hook: + rm -f $(DESTDIR)$(bindir)/plist2json + endif diff --git a/tools/plistutil.c b/tools/plistutil.c index bdf195e..fef72b7 100644 --- a/tools/plistutil.c +++ b/tools/plistutil.c @@ -52,6 +52,7 @@ typedef struct _options #define OPT_DEBUG (1 << 0) #define OPT_COMPACT (1 << 1) #define OPT_SORT (1 << 2) +#define OPT_COERCE (1 << 3) static void print_usage(int argc, char *argv[]) { @@ -74,6 +75,10 @@ static void print_usage(int argc, char *argv[]) printf(" -n, --nodepath PATH Restrict output to nodepath defined by PATH.\n"); printf(" -c, --compact JSON and OpenStep only: Print output in compact form.\n"); printf(" By default, the output will be pretty-printed.\n"); + printf(" -C, --coerce JSON only: Coerce non-JSON plist types to JSON-compatible\n"); + printf(" representations. Date values become ISO 8601 strings,\n"); + printf(" data values become Base64-encoded strings, and UID values\n"); + printf(" become integers. Implied when invoked as plist2json.\n"); printf(" -s, --sort Sort all dictionary nodes lexicographically by key\n"); printf(" before converting to the output format.\n"); printf(" -d, --debug Enable extended debug output\n"); @@ -96,6 +101,7 @@ static options_t *parse_arguments(int argc, char *argv[]) { "outfile", required_argument, 0, 'o' }, { "format", required_argument, 0, 'f' }, { "compact", no_argument, 0, 'c' }, + { "coerce", no_argument, 0, 'C' }, { "sort", no_argument, 0, 's' }, { "print", required_argument, 0, 'p' }, { "nodepath", required_argument, 0, 'n' }, @@ -106,7 +112,7 @@ static options_t *parse_arguments(int argc, char *argv[]) }; int c; - while ((c = getopt_long(argc, argv, "i:o:f:csp:n:dhv", long_options, NULL)) != -1) + while ((c = getopt_long(argc, argv, "i:o:f:cCsp:n:dhv", long_options, NULL)) != -1) { switch (c) { @@ -154,6 +160,10 @@ static options_t *parse_arguments(int argc, char *argv[]) options->flags |= OPT_COMPACT; break; + case 'C': + options->flags |= OPT_COERCE; + break; + case 's': options->flags |= OPT_SORT; break; @@ -230,6 +240,18 @@ int main(int argc, char *argv[]) return 0; } + // detect invocation as plist2json symlink + { + char *progname = strrchr(argv[0], '/'); + progname = progname ? progname + 1 : argv[0]; + if (!strcmp(progname, "plist2json")) { + if (options->out_fmt == 0) { + options->out_fmt = PLIST_FORMAT_JSON; + } + options->flags |= OPT_COERCE; + } + } + if (options->flags & OPT_DEBUG) { plist_set_debug(1); @@ -404,7 +426,10 @@ int main(int argc, char *argv[]) } else if (options->out_fmt == PLIST_FORMAT_XML) { output_res = plist_to_xml(root_node, &plist_out, &size); } else if (options->out_fmt == PLIST_FORMAT_JSON) { - output_res = plist_to_json(root_node, &plist_out, &size, !(options->flags & OPT_COMPACT)); + plist_write_options_t wropts = PLIST_OPT_NONE; + if (options->flags & OPT_COMPACT) wropts |= PLIST_OPT_COMPACT; + if (options->flags & OPT_COERCE) wropts |= PLIST_OPT_COERCE; + output_res = plist_to_json_with_options(root_node, &plist_out, &size, wropts); } else if (options->out_fmt == PLIST_FORMAT_OSTEP) { output_res = plist_to_openstep(root_node, &plist_out, &size, !(options->flags & OPT_COMPACT)); } else { -- cgit v1.1-32-gdbae