summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Calil Khalil2026-02-21 10:39:24 -0300
committerGravatar Nikias Bassen2026-03-20 17:12:47 +0100
commit3edac28498d883f1f768699ee15ce85a82bb2a7b (patch)
treeb350517a84a671fc500d943168f973e7c97aa93e
parent6e03a1df6d1aa87c8f9e2b35f1a2ca60feca1c0e (diff)
downloadlibplist-3edac28498d883f1f768699ee15ce85a82bb2a7b.tar.gz
libplist-3edac28498d883f1f768699ee15ce85a82bb2a7b.tar.bz2
Add JSON coercion support for non-JSON plist typesHEADmaster
- 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
-rw-r--r--include/plist/plist.h28
-rw-r--r--src/jplist.c129
-rw-r--r--src/plist.c4
-rw-r--r--tools/Makefile.am6
-rw-r--r--tools/plistutil.c29
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. */
@@ -938,6 +943,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.
*
* @param plist the root node to export
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,17 +483,23 @@ 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 {