Add support for OpenStep plist format
diff --git a/docs/plistutil.1 b/docs/plistutil.1
index eb1b591..5342e91 100644
--- a/docs/plistutil.1
+++ b/docs/plistutil.1
@@ -18,13 +18,17 @@
 Output FILE to convert to. If this argument is omitted or - is passed as
 filename, plistutil will write to stdout.
 .TP
-.B \-f, \-\-format [bin|xml|json]
+.B \-f, \-\-format [bin|xml|json|openstep]
 Force output format, regardless of input type. This is useful if the input
 format is not known, but the output format should always be in a specific
 format (like xml or json).
 
 If omitted, XML plist data will be converted to binary and vice-versa. To
-convert to/from JSON the output format needs to specified.
+convert to/from JSON or OpenStep the output format needs to specified.
+.TP
+.B \-c, \-\-compact
+JSON and OpenStep only: Print output in compact form. By default, the output
+will be pretty-printed.
 .TP
 .B \-h, \-\-help
 Prints usage information.
diff --git a/include/plist/plist.h b/include/plist/plist.h
index c0eae1c..0ae8889 100644
--- a/include/plist/plist.h
+++ b/include/plist/plist.h
@@ -698,6 +698,20 @@
     plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, int prettify);
 
     /**
+     * Export the #plist_t structure to OpenStep format.
+     *
+     * @param plist the root node to export
+     * @param plist_openstep 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 prettify pretty print the output if != 0
+     * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+     * @note Use plist_mem_free() to free the allocated memory.
+     */
+    plist_err_t plist_to_openstep(plist_t plist, char **plist_openstep, uint32_t* length, int prettify);
+
+
+    /**
      * Import the #plist_t structure from XML format.
      *
      * @param plist_xml a pointer to the xml buffer.
@@ -728,6 +742,16 @@
     plist_err_t plist_from_json(const char *json, uint32_t length, plist_t * plist);
 
     /**
+     * Import the #plist_t structure from OpenStep plist format.
+     *
+     * @param openstep a pointer to the OpenStep plist buffer.
+     * @param length length of the buffer to read.
+     * @param plist a pointer to the imported plist.
+     * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+     */
+    plist_err_t plist_from_openstep(const char *openstep, uint32_t length, plist_t * plist);
+
+    /**
      * Import the #plist_t structure from memory data.
      * This method will look at the first bytes of plist_data
      * to determine if plist_data contains a binary, JSON, or XML plist
diff --git a/src/Makefile.am b/src/Makefile.am
index d4c9e67..02b516c 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -24,6 +24,7 @@
 	bplist.c \
 	jsmn.c jsmn.h \
 	jplist.c \
+	oplist.c \
 	plist.c plist.h
 
 libplist___2_0_la_LIBADD = libplist-2.0.la
diff --git a/src/oplist.c b/src/oplist.c
new file mode 100644
index 0000000..fa6977a
--- /dev/null
+++ b/src/oplist.c
@@ -0,0 +1,861 @@
+/*
+ * oplist.c
+ * OpenStep plist implementation
+ *
+ * Copyright (c) 2021-2022 Nikias Bassen, All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+
+#include <inttypes.h>
+#include <ctype.h>
+#include <math.h>
+#include <limits.h>
+
+#include <node.h>
+#include <node_list.h>
+
+#include "plist.h"
+#include "strbuf.h"
+
+#ifdef DEBUG
+static int plist_ostep_debug = 0;
+#define PLIST_OSTEP_ERR(...) if (plist_ostep_debug) { fprintf(stderr, "libplist[ostepparser] ERROR: " __VA_ARGS__); }
+#define PLIST_OSTEP_WRITE_ERR(...) if (plist_ostep_debug) { fprintf(stderr, "libplist[ostepwriter] ERROR: " __VA_ARGS__); }
+#else
+#define PLIST_OSTEP_ERR(...)
+#define PLIST_OSTEP_WRITE_ERR(...)
+#endif
+
+void plist_ostep_init(void)
+{
+    /* init OpenStep stuff */
+#ifdef DEBUG
+    char *env_debug = getenv("PLIST_OSTEP_DEBUG");
+    if (env_debug && !strcmp(env_debug, "1")) {
+        plist_ostep_debug = 1;
+    }
+#endif
+}
+
+void plist_ostep_deinit(void)
+{
+    /* deinit OpenStep plist stuff */
+}
+
+#ifndef HAVE_STRNDUP
+static char* strndup(const char* str, size_t len)
+{
+    char *newstr = (char *)malloc(len+1);
+    if (newstr) {
+        strncpy(newstr, str, len);
+        newstr[len]= '\0';
+    }
+    return newstr;
+}
+#endif
+
+static size_t dtostr(char *buf, size_t bufsize, double realval)
+{
+    size_t len = 0;
+    if (isnan(realval)) {
+        len = snprintf(buf, bufsize, "nan");
+    } else if (isinf(realval)) {
+        len = snprintf(buf, bufsize, "%cinfinity", (realval > 0.0) ? '+' : '-');
+    } else if (realval == 0.0f) {
+        len = snprintf(buf, bufsize, "0.0");
+    } else {
+        size_t i = 0;
+        len = snprintf(buf, bufsize, "%.*g", 17, realval);
+        for (i = 0; buf && i < len; i++) {
+            if (buf[i] == ',') {
+                buf[i] = '.';
+                break;
+            } else if (buf[i] == '.') {
+                break;
+            }
+        }
+    }
+    return len;
+}
+
+static const char allowed_unquoted_chars[256] = {
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static int str_needs_quotes(const char* str, size_t len)
+{
+    size_t i;
+    for (i = 0; i < len; i++) {
+        if (!allowed_unquoted_chars[(unsigned char)str[i]]) {
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int node_to_openstep(node_t* node, bytearray_t **outbuf, uint32_t depth, int prettify)
+{
+    plist_data_t node_data = NULL;
+
+    char *val = NULL;
+    size_t val_len = 0;
+
+    uint32_t i = 0;
+
+    if (!node)
+        return PLIST_ERR_INVALID_ARG;
+
+    node_data = plist_get_data(node);
+
+    switch (node_data->type)
+    {
+    case PLIST_UINT:
+        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);
+        break;
+
+    case PLIST_REAL:
+        val = (char*)malloc(64);
+        val_len = dtostr(val, 64, node_data->realval);
+        str_buf_append(*outbuf, val, val_len);
+        free(val);
+        break;
+
+    case PLIST_STRING:
+    case PLIST_KEY: {
+        const char *charmap[32] = {
+            "\\U0000", "\\U0001", "\\U0002", "\\U0003", "\\U0004", "\\U0005", "\\U0006", "\\U0007",
+            "\\b",     "\\t",     "\\n",     "\\U000b", "\\f",     "\\r",     "\\U000e", "\\U000f",
+            "\\U0010", "\\U0011", "\\U0012", "\\U0013", "\\U0014", "\\U0015", "\\U0016", "\\U0017",
+            "\\U0018", "\\U0019", "\\U001a", "\\U001b", "\\U001c", "\\U001d", "\\U001e", "\\U001f",
+        };
+        size_t j = 0;
+        size_t len = 0;
+        off_t start = 0;
+        off_t cur = 0;
+        int needs_quotes;
+
+        len = node_data->length;
+
+        needs_quotes = str_needs_quotes(node_data->strval, len);
+
+        if (needs_quotes) {
+            str_buf_append(*outbuf, "\"", 1);
+        }
+
+        for (j = 0; j < len; j++) {
+            unsigned char ch = (unsigned char)node_data->strval[j];
+            if (ch < 0x20) {
+                str_buf_append(*outbuf, node_data->strval + start, cur - start);
+                str_buf_append(*outbuf, charmap[ch], (charmap[ch][1] == 'u') ? 6 : 2);
+                start = cur+1;
+            } else if (ch == '"') {
+                str_buf_append(*outbuf, node_data->strval + start, cur - start);
+                str_buf_append(*outbuf, "\\\"", 2);
+                start = cur+1;
+            }
+            cur++;
+        }
+        str_buf_append(*outbuf, node_data->strval + start, cur - start);
+
+        if (needs_quotes) {
+            str_buf_append(*outbuf, "\"", 1);
+        }
+
+        } break;
+
+    case PLIST_ARRAY: {
+        str_buf_append(*outbuf, "(", 1);
+        node_t *ch;
+        uint32_t cnt = 0;
+        for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) {
+            if (cnt > 0) {
+                str_buf_append(*outbuf, ",", 1);
+            }
+            if (prettify) {
+                str_buf_append(*outbuf, "\n", 1);
+                for (i = 0; i <= depth; i++) {
+                    str_buf_append(*outbuf, "  ", 2);
+                }
+            }
+            int res = node_to_openstep(ch, outbuf, depth+1, prettify);
+            if (res < 0) {
+                return res;
+            }
+            cnt++;
+        }
+        if (cnt > 0 && prettify) {
+            str_buf_append(*outbuf, "\n", 1);
+            for (i = 0; i < depth; i++) {
+                str_buf_append(*outbuf, "  ", 2);
+            }
+        }
+        str_buf_append(*outbuf, ")", 1);
+        } break;
+    case PLIST_DICT: {
+        str_buf_append(*outbuf, "{", 1);
+        node_t *ch;
+        uint32_t cnt = 0;
+        for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) {
+            if (cnt > 0 && cnt % 2 == 0) {
+                str_buf_append(*outbuf, ";", 1);
+            }
+            if (cnt % 2 == 0 && prettify) {
+                str_buf_append(*outbuf, "\n", 1);
+                for (i = 0; i <= depth; i++) {
+                    str_buf_append(*outbuf, "  ", 2);
+                }
+            }
+            int res = node_to_openstep(ch, outbuf, depth+1, prettify);
+            if (res < 0) {
+                return res;
+            }
+            if (cnt % 2 == 0) {
+                if (prettify) {
+                  str_buf_append(*outbuf, " = ", 3);
+                } else {
+                  str_buf_append(*outbuf, "=", 1);
+                }
+            }
+            cnt++;
+        }
+        if (cnt > 0) {
+          str_buf_append(*outbuf, ";", 1);
+        }
+        if (cnt > 0 && prettify) {
+            str_buf_append(*outbuf, "\n", 1);
+            for (i = 0; i < depth; i++) {
+                str_buf_append(*outbuf, "  ", 2);
+            }
+        }
+        str_buf_append(*outbuf, "}", 1);
+        } break;
+    case PLIST_DATA: {
+        size_t j = 0;
+        size_t len = 0;
+        str_buf_append(*outbuf, "<", 1);
+        len = node_data->length;
+        for (j = 0; j < len; j++) {
+            char charb[4];
+            if (prettify && j > 0 && (j % 4 == 0))
+              str_buf_append(*outbuf, " ", 1);
+            sprintf(charb, "%02x", (unsigned char)node_data->buff[j]);
+            str_buf_append(*outbuf, charb, 2);
+        }
+        str_buf_append(*outbuf, ">", 1);
+        } break;
+    case PLIST_BOOLEAN:
+        PLIST_OSTEP_WRITE_ERR("PLIST_BOOLEAN type is not valid for OpenStep format\n");
+        return PLIST_ERR_FORMAT;
+    case PLIST_NULL:
+        PLIST_OSTEP_WRITE_ERR("PLIST_NULL type is not valid for OpenStep format\n");
+        return PLIST_ERR_FORMAT;
+    case PLIST_DATE:
+        // NOT VALID FOR OPENSTEP
+        PLIST_OSTEP_WRITE_ERR("PLIST_DATE type is not valid for OpenStep format\n");
+        return PLIST_ERR_FORMAT;
+    case PLIST_UID:
+        // NOT VALID FOR OPENSTEP
+        PLIST_OSTEP_WRITE_ERR("PLIST_UID type is not valid for OpenStep format\n");
+        return PLIST_ERR_FORMAT;
+    default:
+        return PLIST_ERR_UNKNOWN;
+    }
+
+    return PLIST_ERR_SUCCESS;
+}
+
+#define PO10i_LIMIT (INT64_MAX/10)
+
+/* based on https://stackoverflow.com/a/4143288 */
+static int num_digits_i(int64_t i)
+{
+    int n;
+    int64_t po10;
+    n=1;
+    if (i < 0) {
+        i = (i == INT64_MIN) ? INT64_MAX : -i;
+        n++;
+    }
+    po10=10;
+    while (i>=po10) {
+        n++;
+        if (po10 > PO10i_LIMIT) break;
+        po10*=10;
+    }
+    return n;
+}
+
+#define PO10u_LIMIT (UINT64_MAX/10)
+
+/* based on https://stackoverflow.com/a/4143288 */
+static int num_digits_u(uint64_t i)
+{
+    int n;
+    uint64_t po10;
+    n=1;
+    po10=10;
+    while (i>=po10) {
+        n++;
+        if (po10 > PO10u_LIMIT) break;
+        po10*=10;
+    }
+    return n;
+}
+
+static int node_estimate_size(node_t *node, uint64_t *size, uint32_t depth, int prettify)
+{
+    plist_data_t data;
+    if (!node) {
+        return PLIST_ERR_INVALID_ARG;
+    }
+    data = plist_get_data(node);
+    if (node->children) {
+        node_t *ch;
+        unsigned int n_children = node_n_children(node);
+        for (ch = node_first_child(node); ch; ch = node_next_sibling(ch)) {
+            int res = node_estimate_size(ch, size, depth + 1, prettify);
+            if (res < 0) {
+                return res;
+            }
+        }
+        switch (data->type) {
+        case PLIST_DICT:
+            *size += 2; // '{' and '}'
+            *size += n_children; // number of '=' and ';'
+            if (prettify) {
+                *size += n_children*2; // number of '\n' and extra spaces
+                *size += (uint64_t)n_children * (depth+1); // indent for every 2nd child
+                *size += 1; // additional '\n'
+            }
+            break;
+        case PLIST_ARRAY:
+            *size += 2; // '(' and ')'
+            *size += n_children-1; // number of ','
+            if (prettify) {
+                *size += n_children; // number of '\n'
+                *size += (uint64_t)n_children * ((depth+1)<<1); // indent for every child
+                *size += 1; // additional '\n'
+            }
+            break;
+        default:
+            break;
+	}
+        if (prettify)
+            *size += (depth << 1); // indent for {} and ()
+    } else {
+        switch (data->type) {
+        case PLIST_STRING:
+        case PLIST_KEY:
+            *size += data->length;
+            *size += 2;
+            break;
+        case PLIST_UINT:
+            if (data->length == 16) {
+                *size += num_digits_u(data->intval);
+            } else {
+                *size += num_digits_i((int64_t)data->intval);
+            }
+            break;
+        case PLIST_REAL:
+            *size += dtostr(NULL, 0, data->realval);
+            break;
+        case PLIST_DICT:
+        case PLIST_ARRAY:
+            *size += 2;
+            break;
+        case PLIST_DATA:
+            *size += 2; // < and >
+            *size += data->length*2;
+            if (prettify)
+                *size += data->length/4;
+            break;
+        case PLIST_BOOLEAN:
+            // NOT VALID FOR OPENSTEP
+            PLIST_OSTEP_WRITE_ERR("PLIST_BOOLEAN type is not valid for OpenStep format\n");
+            return PLIST_ERR_FORMAT;
+        case PLIST_DATE:
+            // NOT VALID FOR OPENSTEP
+            PLIST_OSTEP_WRITE_ERR("PLIST_DATE type is not valid for OpenStep format\n");
+            return PLIST_ERR_FORMAT;
+        case PLIST_UID:
+            // NOT VALID FOR OPENSTEP
+            PLIST_OSTEP_WRITE_ERR("PLIST_UID type is not valid for OpenStep format\n");
+            return PLIST_ERR_FORMAT;
+        default:
+            PLIST_OSTEP_WRITE_ERR("invalid node type encountered\n");
+            return PLIST_ERR_UNKNOWN;
+        }
+    }
+    return PLIST_ERR_SUCCESS;
+}
+
+PLIST_API int plist_to_openstep(plist_t plist, char **openstep, uint32_t* length, int prettify)
+{
+    uint64_t size = 0;
+    int res;
+
+    if (!plist || !openstep || !length) {
+        return PLIST_ERR_INVALID_ARG;
+    }
+
+    res = node_estimate_size(plist, &size, 0, prettify);
+    if (res < 0) {
+        return res;
+    }
+
+    strbuf_t *outbuf = str_buf_new(size);
+    if (!outbuf) {
+        PLIST_OSTEP_WRITE_ERR("Could not allocate output buffer");
+        return PLIST_ERR_NO_MEM;
+    }
+
+    res = node_to_openstep(plist, &outbuf, 0, prettify);
+    if (res < 0) {
+        str_buf_free(outbuf);
+        *openstep = NULL;
+        *length = 0;
+        return res;
+    }
+    if (prettify) {
+        str_buf_append(outbuf, "\n", 1);
+    }
+
+    str_buf_append(outbuf, "\0", 1);
+
+    *openstep = outbuf->data;
+    *length = outbuf->len - 1;
+
+    outbuf->data = NULL;
+    str_buf_free(outbuf);
+
+    return PLIST_ERR_SUCCESS;
+}
+
+struct _parse_ctx {
+    const char *start;
+    const char *pos;
+    const char *end;
+    int err;
+};
+typedef struct _parse_ctx* parse_ctx;
+
+static void parse_skip_ws(parse_ctx ctx)
+{
+    while (ctx->pos < ctx->end) {
+        // skip comments
+        if (*ctx->pos == '/' && (ctx->end - ctx->pos > 1)) {
+            if (*(ctx->pos+1) == '/') {
+                ctx->pos++;
+                while (ctx->pos < ctx->end) {
+                    if ((*ctx->pos == '\n') || (*ctx->pos == '\r')) {
+                        break;
+                    }
+                    ctx->pos++;
+                }
+            } else if (*(ctx->pos+1) == '*') {
+                ctx->pos++;
+                while (ctx->pos < ctx->end) {
+                    if (*ctx->pos == '*' && (ctx->end - ctx->pos > 1)) {
+                        if (*(ctx->pos+1) == '/') {
+                            ctx->pos+=2;
+                            break;
+                        }
+                    }
+                    ctx->pos++;
+                }
+            }
+        }
+        // break on any char that's not white space
+        if (!(((*(ctx->pos) == ' ') || (*(ctx->pos) == '\t') || (*(ctx->pos) == '\r') || (*(ctx->pos) == '\n')))) {
+            break;
+        }
+        ctx->pos++;
+    }
+}
+
+#define HEX_DIGIT(x) ((x <= '9') ? (x - '0') : ((x <= 'F') ? (x - 'A' + 10) : (x - 'a' + 10)))
+
+static int node_from_openstep(parse_ctx ctx, plist_t *plist);
+
+static void parse_dict_data(parse_ctx ctx, plist_t dict)
+{
+    plist_t key = NULL;
+    plist_t val = NULL;
+    while (ctx->pos < ctx->end && !ctx->err) {
+        parse_skip_ws(ctx);
+        if (*ctx->pos == '}' || ctx->pos >= ctx->end) {
+            break;
+        }
+        key = NULL;
+        ctx->err = node_from_openstep(ctx, &key);
+        if (ctx->err != 0) {
+            break;
+        }
+        if (!PLIST_IS_STRING(key)) {
+            PLIST_OSTEP_ERR("Invalid type for dictionary key at offset %ld\n", ctx->pos - ctx->start);
+            ctx->err++;
+            break;
+        }
+        parse_skip_ws(ctx);
+        if (*ctx->pos != '=') {
+            PLIST_OSTEP_ERR("Missing '=' while parsing dictionary item at offset %ld\n", ctx->pos - ctx->start);
+            ctx->err++;
+            break;
+        }
+        ctx->pos++;
+        if (ctx->pos >= ctx->end) {
+            PLIST_OSTEP_ERR("EOF while parsing dictionary item at offset %ld\n", ctx->pos - ctx->start);
+            ctx->err++;
+                    break;
+        }
+        val = NULL;
+        ctx->err = node_from_openstep(ctx, &val);
+        if (ctx->err != 0) {
+            plist_free(key);
+            break;
+        }
+        if (!val) {
+            plist_free(key);
+            PLIST_OSTEP_ERR("Missing value for dictionary item at offset %ld\n", ctx->pos - ctx->start);
+            ctx->err++;
+            break;
+        }
+        parse_skip_ws(ctx);
+        if (*ctx->pos != ';') {
+            plist_free(val);
+            plist_free(key);
+            PLIST_OSTEP_ERR("Missing terminating ';' while parsing dictionary item at offset %ld\n", ctx->pos - ctx->start);
+            ctx->err++;
+            break;
+        }
+
+        plist_dict_set_item(dict, plist_get_string_ptr(key, NULL), val);
+        plist_free(key);
+        val = NULL;
+
+        ctx->pos++;
+    }
+}
+
+static int node_from_openstep(parse_ctx ctx, plist_t *plist)
+{
+    plist_t subnode = NULL;
+    const char *p = NULL;
+    while (ctx->pos < ctx->end && !ctx->err) {
+        parse_skip_ws(ctx);
+        if (ctx->pos >= ctx->end) {
+            break;
+        }
+        plist_data_t data = plist_new_plist_data();
+        if (*ctx->pos == '{') {
+            data->type = PLIST_DICT;
+            subnode = plist_new_node(data);
+            ctx->pos++;
+            parse_dict_data(ctx, subnode);
+            if (ctx->err) {
+                goto err_out;
+            }
+            if (*ctx->pos != '}') {
+                PLIST_OSTEP_ERR("Missing terminating '}' at offset %ld\n", ctx->pos - ctx->start);
+                ctx->err++;
+                goto err_out;
+            }
+            ctx->pos++;
+            *plist = subnode;
+            parse_skip_ws(ctx);
+            break;
+        } else if (*ctx->pos == '(') {
+            data->type = PLIST_ARRAY;
+            subnode = plist_new_node(data);
+            ctx->pos++;
+            plist_t tmp = NULL;
+            while (ctx->pos < ctx->end && !ctx->err) {
+                parse_skip_ws(ctx);
+                if (*ctx->pos == ')') {
+                    break;
+                }
+                ctx->err = node_from_openstep(ctx, &tmp);
+                if (ctx->err != 0) {
+                    break;
+                }
+                if (!tmp) {
+                    ctx->err++;
+                    break;
+                }
+                plist_array_append_item(subnode, tmp);
+                tmp = NULL;
+                parse_skip_ws(ctx);
+                if (*ctx->pos != ',') {
+                    break;
+                }
+                ctx->pos++;
+            }
+            if (ctx->err) {
+                goto err_out;
+            }
+            if (*ctx->pos != ')') {
+                PLIST_OSTEP_ERR("Missing terminating ')' at offset %ld\n", ctx->pos - ctx->start);
+                ctx->err++;
+                goto err_out;
+            }
+            ctx->pos++;
+            *plist = subnode;
+            parse_skip_ws(ctx);
+            break;
+        } else if (*ctx->pos == '<') {
+            data->type = PLIST_DATA;
+            ctx->pos++;
+            bytearray_t *bytes = byte_array_new(256);
+            while (ctx->pos < ctx->end && !ctx->err) {
+                parse_skip_ws(ctx);
+                if (*ctx->pos == '>') {
+                    break;
+                }
+                if (!isxdigit(*ctx->pos)) {
+                    PLIST_OSTEP_ERR("Invalid byte group in data at offset %ld\n", ctx->pos - ctx->start);
+                    ctx->err++;
+                    break;
+                }
+                uint8_t b = HEX_DIGIT(*ctx->pos);
+                ctx->pos++;
+                if (ctx->pos >= ctx->end) {
+                    PLIST_OSTEP_ERR("Unexpected end of data at offset %ld\n", ctx->pos - ctx->start);
+                    ctx->err++;
+                    break;
+                }
+                if (!isxdigit(*ctx->pos)) {
+                    PLIST_OSTEP_ERR("Invalid byte group in data at offset %ld\n", ctx->pos - ctx->start);
+                    ctx->err++;
+                    break;
+                }
+                b = (b << 4) + HEX_DIGIT(*ctx->pos);
+                byte_array_append(bytes, &b, 1);
+                ctx->pos++;
+            }
+            if (ctx->err) {
+                goto err_out;
+            }
+            if (*ctx->pos != '>') {
+                PLIST_OSTEP_ERR("Missing terminating '>' at offset %ld\n", ctx->pos - ctx->start);
+                ctx->err++;
+                goto err_out;
+            }
+            ctx->pos++;
+            data->buff = bytes->data;
+            data->length = bytes->len;
+            bytes->data = NULL;
+            byte_array_free(bytes);
+            *plist = plist_new_node(data);
+            parse_skip_ws(ctx);
+            break;
+        } else if (*ctx->pos == '"' || *ctx->pos == '\'') {
+            char c = *ctx->pos;
+            ctx->pos++;
+            p = ctx->pos;
+            int num_escapes = 0;
+            while (ctx->pos < ctx->end) {
+                if (*ctx->pos == '\\') {
+                    num_escapes++;
+                }
+                if ((*ctx->pos == c) && (*(ctx->pos-1) != '\\')) {
+                    break;
+                }
+                ctx->pos++;
+            }
+            if (*ctx->pos != c) {
+                PLIST_OSTEP_ERR("Missing closing quote (%c) at offset %ld\n", c, ctx->pos - ctx->start);
+                ctx->err++;
+                goto err_out;
+            }
+            size_t slen = ctx->pos - p;
+            ctx->pos++; // skip the closing quote
+            char* strbuf = malloc(slen+1);
+            if (num_escapes > 0) {
+                size_t i = 0;
+                size_t o = 0;
+                while (i < slen) {
+                    if (p[i] == '\\') {
+                        /* handle escape sequence */
+                        i++;
+                        switch (p[i]) {
+                            case '0':
+                            case '1':
+                            case '2':
+                            case '3':
+                            case '4':
+                            case '5':
+                            case '6':
+                            case '7': {
+                                // max 3 digits octal
+                                unsigned char chr = 0;
+                                int maxd = 3;
+                                while ((i < slen) && (p[i] >= '0' && p[i] <= '7') && --maxd) {
+                                    chr = (chr << 3) + p[i] - '0';
+                                    i++;
+                                }
+                                strbuf[o++] = (char)chr;
+                            }   break;
+                            case 'U': {
+                                i++;
+                                // max 4 digits hex
+                                uint16_t wchr = 0;
+                                int maxd = 4;
+                                while ((i < slen) && isxdigit(p[i]) && maxd--) {
+                                    wchr = (wchr << 4) + ((p[i] <= '9') ? (p[i] - '0') : ((p[i] <= 'F') ? (p[i] - 'A' + 10) : (p[i] - 'a' + 10)));
+                                    i++;
+                                }
+                                if (wchr >= 0x800) {
+                                    strbuf[o++] = (char)(0xE0 + ((wchr >> 12) & 0xF));
+                                    strbuf[o++] = (char)(0x80 + ((wchr >> 6) & 0x3F));
+                                    strbuf[o++] = (char)(0x80 + (wchr & 0x3F));
+                                } else if (wchr >= 0x80) {
+                                    strbuf[o++] = (char)(0xC0 + ((wchr >> 6) & 0x1F));
+                                    strbuf[o++] = (char)(0x80 + (wchr & 0x3F));
+                                } else {
+                                    strbuf[o++] = (char)(wchr & 0x7F);
+                                }
+                            }   break;
+                            case 'a': strbuf[o++] = '\a'; i++; break;
+                            case 'b': strbuf[o++] = '\b'; i++; break;
+                            case 'f': strbuf[o++] = '\f'; i++; break;
+                            case 'n': strbuf[o++] = '\n'; i++; break;
+                            case 'r': strbuf[o++] = '\r'; i++; break;
+                            case 't': strbuf[o++] = '\t'; i++; break;
+                            case 'v': strbuf[o++] = '\v'; i++; break;
+                            case '"': strbuf[o++] = '"';  i++; break;
+                            case '\'': strbuf[o++] = '\''; i++; break;
+                            default:
+                                break;
+                        }
+                    } else {
+                        strbuf[o++] = p[i++];
+                    }
+                }
+                strbuf[o] = '\0';
+                slen = o;
+            } else {
+                strncpy(strbuf, p, slen);
+                strbuf[slen] = '\0';
+            }
+            data->type = PLIST_STRING;
+            data->strval = strbuf;
+            data->length = slen;
+            *plist = plist_new_node(data);
+            parse_skip_ws(ctx);
+            break;
+        } else {
+            // unquoted string
+            size_t slen = 0;
+            parse_skip_ws(ctx);
+            p = ctx->pos;
+            while (ctx->pos < ctx->end) {
+                if (!allowed_unquoted_chars[(uint8_t)*ctx->pos]) {
+                    break;
+                }
+                ctx->pos++;
+            }
+            slen = ctx->pos-p;
+            if (slen > 0) {
+                data->type = PLIST_STRING;
+                data->strval = strndup(p, slen);
+                data->length = slen;
+                *plist = plist_new_node(data);
+                parse_skip_ws(ctx);
+                break;
+            } else {
+                PLIST_OSTEP_ERR("Unexpected character when parsing unquoted string at offset %ld\n", ctx->pos - ctx->start);
+                ctx->err++;
+                break;
+            }
+        }
+        ctx->pos++;
+    }
+
+err_out:
+    if (ctx->err) {
+        plist_free(*plist);
+        *plist = NULL;
+        return PLIST_ERR_PARSE;
+    }
+    return PLIST_ERR_SUCCESS;
+}
+
+PLIST_API int plist_from_openstep(const char *plist_ostep, uint32_t length, plist_t * plist)
+{
+    if (!plist) {
+        return PLIST_ERR_INVALID_ARG;
+    }
+    *plist = NULL;
+    if (!plist_ostep || (length == 0)) {
+        return PLIST_ERR_INVALID_ARG;
+    }
+
+    struct _parse_ctx ctx = { plist_ostep, plist_ostep, plist_ostep + length, 0 };
+
+    int err = node_from_openstep(&ctx, plist);
+    if (err == 0) {
+        if (!*plist) {
+            /* whitespace only file is considered an empty dictionary */
+            *plist = plist_new_dict();
+        } else if (ctx.pos < ctx.end && *ctx.pos == '=') {
+            /* attempt to parse this as 'strings' data */
+            plist_free(*plist);
+            plist_t pl = plist_new_dict();
+            ctx.pos = plist_ostep;
+            parse_dict_data(&ctx, pl);
+            if (ctx.err > 0) {
+                plist_free(pl);
+                PLIST_OSTEP_ERR("Failed to parse strings data\n");
+                err = PLIST_ERR_PARSE;
+            } else {
+                *plist = pl;
+            }
+        }
+    }
+
+    return err;
+}
diff --git a/src/plist.c b/src/plist.c
index 37edfa4..e696f70 100644
--- a/src/plist.c
+++ b/src/plist.c
@@ -34,6 +34,7 @@
 #include <assert.h>
 #include <limits.h>
 #include <float.h>
+#include <ctype.h>
 
 #ifdef WIN32
 #include <windows.h>
@@ -51,12 +52,15 @@
 extern void plist_bin_deinit(void);
 extern void plist_json_init(void);
 extern void plist_json_deinit(void);
+extern void plist_ostep_init(void);
+extern void plist_ostep_deinit(void);
 
 static void internal_plist_init(void)
 {
     plist_bin_init();
     plist_xml_init();
     plist_json_init();
+    plist_ostep_init();
 }
 
 static void internal_plist_deinit(void)
@@ -64,6 +68,7 @@
     plist_bin_deinit();
     plist_xml_deinit();
     plist_json_deinit();
+    plist_ostep_deinit();
 }
 
 #ifdef WIN32
@@ -186,6 +191,10 @@
     return (memcmp(plist_data, "bplist00", 8) == 0);
 }
 
+#define SKIP_WS(blob, pos, len) \
+    while (pos < len && ((blob[pos] == ' ') || (blob[pos] == '\t') || (blob[pos] == '\r') || (blob[pos] == '\n'))) pos++;
+#define FIND_NEXT(blob, pos, len, chr) \
+    while (pos < len && (blob[pos] != chr)) pos++;
 
 PLIST_API plist_err_t plist_from_memory(const char *plist_data, uint32_t length, plist_t * plist)
 {
@@ -194,19 +203,54 @@
         return PLIST_ERR_INVALID_ARG;
     }
     *plist = NULL;
-    if (!plist_data || length < 8) {
+    if (!plist_data || length == 0) {
         return PLIST_ERR_INVALID_ARG;
     }
     if (plist_is_binary(plist_data, length)) {
         res = plist_from_bin(plist_data, length, plist);
     } else {
-        /* skip whitespace before checking */
         uint32_t pos = 0;
-        while (pos < length && ((plist_data[pos] == ' ') || (plist_data[pos] == '\t') || (plist_data[pos] == '\r') || (plist_data[pos] == '\n'))) pos++;
-        if (plist_data[pos] == '[' || plist_data[pos] == '{') {
+        int is_json = 0;
+        int is_xml = 0;
+        /* skip whitespace */
+        SKIP_WS(plist_data, pos, length);
+        if (plist_data[pos] == '<' && (length-pos > 3) && !isxdigit(plist_data[pos+1]) && !isxdigit(plist_data[pos+2]) && !isxdigit(plist_data[pos+3])) {
+            is_xml = 1;
+        } else if (plist_data[pos] == '[') {
+            /* only valid for json */
+            is_json = 1;
+        } else if (plist_data[pos] == '(') {
+            /* only valid for openstep */
+        } else if (plist_data[pos] == '{') {
+            /* this could be json or openstep */
+            pos++;
+            SKIP_WS(plist_data, pos, length);
+            if (plist_data[pos] == '"') {
+                /* still could be both */
+                pos++;
+                do {
+                    FIND_NEXT(plist_data, pos, length, '"');
+                    if (plist_data[pos-1] != '\\') {
+                        break;
+                    }
+                    pos++;
+                } while (pos < length);
+                if (plist_data[pos] == '"') {
+                    pos++;
+                    SKIP_WS(plist_data, pos, length);
+                    if (plist_data[pos] == ':') {
+                        /* this is definitely json */
+                        is_json = 1;
+                    }
+                }
+            }
+        }
+        if (is_xml) {
+            res = plist_from_xml(plist_data, length, plist);
+        } else if (is_json) {
             res = plist_from_json(plist_data, length, plist);
         } else {
-            res = plist_from_xml(plist_data, length, plist);
+            res = plist_from_openstep(plist_data, length, plist);
         }
     }
     return res;
diff --git a/test/Makefile.am b/test/Makefile.am
index cd3b940..66543ea 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -9,7 +9,8 @@
 	plist_cmp \
 	plist_test \
 	plist_btest \
-	plist_jtest
+	plist_jtest \
+	plist_otest
 
 plist_cmp_SOURCES = plist_cmp.c
 plist_cmp_LDADD = \
@@ -25,6 +26,9 @@
 plist_jtest_SOURCES = plist_jtest.c
 plist_jtest_LDADD = $(top_builddir)/src/libplist-2.0.la
 
+plist_otest_SOURCES = plist_otest.c
+plist_otest_LDADD = $(top_builddir)/src/libplist-2.0.la
+
 TESTS = \
 	empty.test \
 	small.test \
@@ -54,7 +58,12 @@
 	json2.test \
 	json3.test \
 	json-invalid-types.test \
-	json-int64-min-max.test
+	json-int64-min-max.test \
+	ostep1.test \
+	ostep2.test \
+	ostep-strings.test \
+	ostep-comments.test \
+	ostep-invalid-types.test
 
 EXTRA_DIST = \
 	$(TESTS) \
@@ -102,7 +111,10 @@
 	data/data.bplist \
 	data/j1.json \
 	data/j2.json \
-	data/int64_min_max.json
+	data/int64_min_max.json \
+	data/o1.ostep \
+	data/o2.ostep \
+	data/test.strings
 
 TESTS_ENVIRONMENT = \
 	top_srcdir=$(top_srcdir) \
diff --git a/test/data/o1.ostep b/test/data/o1.ostep
new file mode 100644
index 0000000..074406a
--- /dev/null
+++ b/test/data/o1.ostep
@@ -0,0 +1,45 @@
+{
+  "test" = (1,1);
+  foo = (
+    (-1337),
+    (1),
+    (1),
+    (1),
+    (
+      (1),
+      (1),
+      (1),
+      (1),
+      (
+        (1),
+        (1),
+        (1),
+        (1)
+      )
+    )
+  );
+  more = {
+    "a" = "yo";
+     "b" = (
+       {
+         "c" = 0.25;
+       },
+       {
+         "a" = "yo";
+         "b" = (
+           {
+             "c" = 0.25;
+           },
+           {
+             "a" = "yo";
+             "b" = (
+               {
+                 "cd" = -0.25;
+               }
+             );
+           }
+         );
+       }
+     );
+  };
+}
diff --git a/test/data/o2.ostep b/test/data/o2.ostep
new file mode 100644
index 0000000..5f5f3c2
--- /dev/null
+++ b/test/data/o2.ostep
@@ -0,0 +1,17 @@
+{
+  "Some ASCII string" = "Test ASCII String";
+  "Some UTF8 strings" = (
+    "àéèçù",
+    "日本語",
+    "汉语/漢語",
+    "한국어/조선말",
+    "русский язык",
+    "الْعَرَبيّة",
+    "עִבְרִית",
+    "język polski",
+    "हिन्दी",
+  );
+  "Keys & \"entities\"" = "hello world & others <nodes> are fun!?'";
+  "Some Int" = 32434543632;
+  "Some String with Unicode entity" = "Yeah check this: \U1234 !!!";
+}
diff --git a/test/data/o3.ostep b/test/data/o3.ostep
new file mode 100644
index 0000000..b80444d
--- /dev/null
+++ b/test/data/o3.ostep
@@ -0,0 +1,16 @@
+(
+	{
+		AFirstKey = "A First Value";
+		ASecondKey = "A Second Value";
+		// this is the last entry
+	},
+	/*{
+		BFirstKey = "B First Value";
+		BSecondKey = "B Second Value";
+	},*/
+	{
+		CFirstKey = "C First Value"; // "C First Unused Value";
+		// now here is another comment
+		CSecondKey = /* "C Second Value";*/ "C Second Corrected Value";
+	}
+)
diff --git a/test/data/test.strings b/test/data/test.strings
new file mode 100644
index 0000000..6d6ee43
--- /dev/null
+++ b/test/data/test.strings
@@ -0,0 +1,12 @@
+STRINGS_ENTRY = "Whatever";
+FOO = "BAR";
+BAR = Foo;
+ENTRY0 = "àéèçù";
+ENTRY1 = "日本語";
+ENTRY2 = "汉语/漢語";
+ENTRY3 = "한국어/조선말";
+ENTRY4 = "русский язык";
+ENTRY5 = "الْعَرَبيّة";
+ENTRY6 = "עִבְרִית";
+ENTRY7 = "język polski";
+ENTRY8 = "हिन्दी";
diff --git a/test/ostep-comments.test b/test/ostep-comments.test
new file mode 100755
index 0000000..8f7f629
--- /dev/null
+++ b/test/ostep-comments.test
@@ -0,0 +1,20 @@
+## -*- sh -*-
+
+set -e
+
+DATASRC=$top_srcdir/test/data
+DATAOUT=$top_builddir/test/data
+TESTFILE=o3.ostep
+
+if ! test -d "$DATAOUT"; then
+	mkdir -p $DATAOUT
+fi
+
+export PLIST_OSTEP_DEBUG=1
+
+echo "Converting"
+$top_builddir/test/plist_otest $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
+
+echo "Comparing"
+export PLIST_OSTEP_DEBUG=1
+$top_builddir/test/plist_cmp $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
diff --git a/test/ostep-invalid-types.test b/test/ostep-invalid-types.test
new file mode 100755
index 0000000..9222394
--- /dev/null
+++ b/test/ostep-invalid-types.test
@@ -0,0 +1,33 @@
+## -*- sh -*-
+
+DATASRC=$top_srcdir/test/data
+DATAOUT=$top_builddir/test/data
+TESTFILE0=data.bplist
+TESTFILE1=7.plist
+TESTFILE2=uid.bplist
+
+if ! test -d "$DATAOUT"; then
+	mkdir -p $DATAOUT
+fi
+
+export PLIST_OSTEP_DEBUG=1
+
+echo "Converting (failure expected)"
+$top_builddir/tools/plistutil -f openstep -i $DATASRC/$TESTFILE0 -o /dev/null
+if [ $? -neq 2 ]; then
+  exit 1
+fi
+
+echo "Converting (failure expected)"
+$top_builddir/tools/plistutil -f openstepn -i $DATASRC/$TESTFILE1 -o /dev/null
+if [ $? -neq 2 ]; then
+  exit 2
+fi
+
+echo "Converting (failure expected)"
+$top_builddir/tools/plistutil -f openstep -i $DATASRC/$TESTFILE2 -o /dev/null
+if [ $? -neq 2 ]; then
+  exit 3
+fi
+
+exit 0
diff --git a/test/ostep-strings.test b/test/ostep-strings.test
new file mode 100755
index 0000000..5b0a098
--- /dev/null
+++ b/test/ostep-strings.test
@@ -0,0 +1,20 @@
+## -*- sh -*-
+
+set -e
+
+DATASRC=$top_srcdir/test/data
+DATAOUT=$top_builddir/test/data
+TESTFILE=test.strings
+
+if ! test -d "$DATAOUT"; then
+	mkdir -p $DATAOUT
+fi
+
+export PLIST_OSTEP_DEBUG=1
+
+echo "Converting"
+$top_builddir/test/plist_otest $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
+
+echo "Comparing"
+export PLIST_OSTEP_DEBUG=1
+$top_builddir/test/plist_cmp $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
diff --git a/test/ostep1.test b/test/ostep1.test
new file mode 100755
index 0000000..f0d9b51
--- /dev/null
+++ b/test/ostep1.test
@@ -0,0 +1,20 @@
+## -*- sh -*-
+
+set -e
+
+DATASRC=$top_srcdir/test/data
+DATAOUT=$top_builddir/test/data
+TESTFILE=o1.ostep
+
+if ! test -d "$DATAOUT"; then
+	mkdir -p $DATAOUT
+fi
+
+export PLIST_OSTEP_DEBUG=1
+
+echo "Converting"
+$top_builddir/test/plist_otest $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
+
+echo "Comparing"
+export PLIST_OSTEP_DEBUG=1
+$top_builddir/test/plist_cmp $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
diff --git a/test/ostep2.test b/test/ostep2.test
new file mode 100755
index 0000000..1b991c3
--- /dev/null
+++ b/test/ostep2.test
@@ -0,0 +1,19 @@
+## -*- sh -*-
+
+set -e
+
+DATASRC=$top_srcdir/test/data
+DATAOUT=$top_builddir/test/data
+TESTFILE=o2.ostep
+
+if ! test -d "$DATAOUT"; then
+	mkdir -p $DATAOUT
+fi
+
+export PLIST_OTEST_DEBUG=1
+
+echo "Converting"
+$top_builddir/test/plist_otest $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
+
+echo "Comparing"
+$top_builddir/test/plist_cmp $DATASRC/$TESTFILE $DATAOUT/$TESTFILE.out
diff --git a/test/plist_cmp.c b/test/plist_cmp.c
index 2af3f63..1b4a36a 100644
--- a/test/plist_cmp.c
+++ b/test/plist_cmp.c
@@ -127,19 +127,8 @@
     plist_1[size_in1] = '\0';
     plist_2[size_in2] = '\0';
 
-    if (memcmp(plist_1, "bplist00", 8) == 0)
-        plist_from_bin(plist_1, size_in1, &root_node1);
-    else if (plist_1[0] == '[' || plist_1[0] == '{')
-        plist_from_json(plist_1, size_in1, &root_node1);
-    else
-        plist_from_xml(plist_1, size_in1, &root_node1);
-
-    if (memcmp(plist_2, "bplist00", 8) == 0)
-        plist_from_bin(plist_2, size_in2, &root_node2);
-    else if (plist_2[0] == '[' || plist_2[0] == '{')
-        plist_from_json(plist_2, size_in2, &root_node2);
-    else
-        plist_from_xml(plist_2, size_in2, &root_node2);
+    plist_from_memory(plist_1, size_in1, &root_node1);
+    plist_from_memory(plist_2, size_in2, &root_node2);
 
     if (!root_node1 || !root_node2)
     {
diff --git a/test/plist_otest.c b/test/plist_otest.c
new file mode 100644
index 0000000..14168f8
--- /dev/null
+++ b/test/plist_otest.c
@@ -0,0 +1,130 @@
+/*
+ * plist_otest.c
+ * source libplist regression test
+ *
+ * Copyright (c) 2022 Nikias Bassen, All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+
+#include "plist/plist.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#ifdef _MSC_VER
+#pragma warning(disable:4996)
+#endif
+
+
+int main(int argc, char *argv[])
+{
+    FILE *iplist = NULL;
+    plist_t root_node1 = NULL;
+    plist_t root_node2 = NULL;
+    char *plist_ostep = NULL;
+    char *plist_ostep2 = NULL;
+    char *plist_bin = NULL;
+    int size_in = 0;
+    uint32_t size_out = 0;
+    uint32_t size_out2 = 0;
+    char *file_in = NULL;
+    char *file_out = NULL;
+    struct stat filestats;
+    if (argc != 3)
+    {
+        printf("Wrong input\n");
+        return 1;
+    }
+
+    file_in = argv[1];
+    file_out = argv[2];
+    //read input file
+    iplist = fopen(file_in, "rb");
+
+    if (!iplist)
+    {
+        printf("File does not exists\n");
+        return 2;
+    }
+    printf("File %s is open\n", file_in);
+    stat(file_in, &filestats);
+    size_in = filestats.st_size;
+    plist_ostep = (char *) malloc(sizeof(char) * (size_in + 1));
+    fread(plist_ostep, sizeof(char), size_in, iplist);
+    fclose(iplist);
+    plist_ostep[size_in] = 0;
+
+    //convert one format to another
+    plist_from_openstep(plist_ostep, size_in, &root_node1);
+    if (!root_node1)
+    {
+        printf("OpenStep PList parsing failed\n");
+        return 3;
+    }
+
+    printf("OpenStep PList parsing succeeded\n");
+    plist_to_bin(root_node1, &plist_bin, &size_out);
+    if (!plist_bin)
+    {
+        printf("PList BIN writing failed\n");
+        return 4;
+    }
+
+    printf("PList BIN writing succeeded\n");
+    plist_from_bin(plist_bin, size_out, &root_node2);
+    if (!root_node2)
+    {
+        printf("PList BIN parsing failed\n");
+        return 5;
+    }
+
+    printf("PList BIN parsing succeeded\n");
+    plist_to_openstep(root_node2, &plist_ostep2, &size_out2, 0);
+    if (!plist_ostep2)
+    {
+        printf("OpenStep PList writing failed\n");
+        return 8;
+    }
+
+    printf("OpenStep PList writing succeeded\n");
+    if (plist_ostep2)
+    {
+        FILE *oplist = NULL;
+        oplist = fopen(file_out, "wb");
+        fwrite(plist_ostep2, size_out2, sizeof(char), oplist);
+        fclose(oplist);
+    }
+
+    plist_free(root_node1);
+    plist_free(root_node2);
+    free(plist_bin);
+    free(plist_ostep);
+    free(plist_ostep2);
+
+    if ((uint32_t)size_in != size_out2)
+    {
+        printf("Size of input and output is different\n");
+        printf("Input size : %i\n", size_in);
+        printf("Output size : %i\n", size_out2);
+    }
+
+    //success
+    return 0;
+}
+
diff --git a/tools/plistutil.c b/tools/plistutil.c
index 677e432..6254b7c 100644
--- a/tools/plistutil.c
+++ b/tools/plistutil.c
@@ -41,7 +41,9 @@
 typedef struct _options
 {
     char *in_file, *out_file;
-    uint8_t debug, in_fmt, out_fmt; // fmts 0 = undef, 1 = bin, 2 = xml, 3 = json
+    uint8_t debug;
+    uint8_t compact;
+    uint8_t in_fmt, out_fmt; // fmts 0 = undef, 1 = bin, 2 = xml, 3 = json, 4 = openstep
 } options_t;
 
 static void print_usage(int argc, char *argv[])
@@ -50,17 +52,19 @@
     name = strrchr(argv[0], '/');
     printf("Usage: %s [OPTIONS] [-i FILE] [-o FILE]\n", (name ? name + 1: argv[0]));
     printf("\n");
-    printf("Convert a plist FILE between binary, XML, and JSON format.\n");
+    printf("Convert a plist FILE between binary, XML, JSON, and OpenStep format.\n");
     printf("If -f is omitted, XML plist data will be converted to binary and vice-versa.\n");
-    printf("To convert to/from JSON the output format needs to be specified.\n");
+    printf("To convert to/from JSON or OpenStep the output format needs to be specified.\n");
     printf("\n");
     printf("OPTIONS:\n");
     printf("  -i, --infile FILE    Optional FILE to convert from or stdin if - or not used\n");
     printf("  -o, --outfile FILE   Optional FILE to convert to or stdout if - or not used\n");
     printf("  -f, --format FORMAT  Force output format, regardless of input type\n");
-    printf("                       FORMAT is one of xml, bin, or json\n");
-    printf("                       If omitted XML will be converted to binary,\n");
+    printf("                       FORMAT is one of xml, bin, json, or openstep\n");
+    printf("                       If omitted, XML will be converted to binary,\n");
     printf("                       and binary to XML.\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("  -d, --debug          Enable extended debug output\n");
     printf("  -v, --version        Print version information\n");
     printf("\n");
@@ -112,6 +116,8 @@
                 options->out_fmt = 2;
             } else if (!strncmp(argv[i+1], "json", 4)) {
                 options->out_fmt = 3;
+            } else if (!strncmp(argv[i+1], "openstep", 8) || !strncmp(argv[i+1], "ostep", 5)) {
+                options->out_fmt = 4;
             } else {
                 fprintf(stderr, "ERROR: Unsupported output format\n");
                 free(options);
@@ -120,6 +126,10 @@
             i++;
             continue;
         }
+        else if (!strcmp(argv[i], "--compact") || !strcmp(argv[i], "-c"))
+        {
+            options->compact = 1;
+        }
         else if (!strcmp(argv[i], "--debug") || !strcmp(argv[i], "-d"))
         {
             options->debug = 1;
@@ -216,13 +226,6 @@
             free(options);
             return 1;
         }
-
-        if (read_size < 8) {
-            fprintf(stderr, "ERROR: Input file is too small to contain valid plist data.\n");
-            free(plist_entire);
-            free(options);
-            return 1;
-        }
     }
     else
     {
@@ -237,13 +240,6 @@
         memset(&filestats, '\0', sizeof(struct stat));
         fstat(fileno(iplist), &filestats);
 
-        if (filestats.st_size < 8) {
-            fprintf(stderr, "ERROR: Input file is too small to contain valid plist data.\n");
-            free(options);
-            fclose(iplist);
-            return -1;
-        }
-
         plist_entire = (char *) malloc(sizeof(char) * (filestats.st_size + 1));
         read_size = fread(plist_entire, sizeof(char), filestats.st_size, iplist);
         plist_entire[read_size] = '\0';
@@ -276,7 +272,9 @@
             } else if (options->out_fmt == 2) {
                 output_res = plist_to_xml(root_node, &plist_out, &size);
             } else if (options->out_fmt == 3) {
-                output_res = plist_to_json(root_node, &plist_out, &size, 0);
+                output_res = plist_to_json(root_node, &plist_out, &size, !options->compact);
+            } else if (options->out_fmt == 4) {
+                output_res = plist_to_openstep(root_node, &plist_out, &size, !options->compact);
             }
         }
     }
@@ -316,8 +314,20 @@
                 ret = 1;
         }
     } else {
-        fprintf(stderr, "ERROR: Could not parse plist data (%d)\n", input_res);
-        ret = 1;
+        switch (input_res) {
+            case PLIST_ERR_PARSE:
+                if (options->out_fmt == 0) {
+                    fprintf(stderr, "ERROR: Could not parse plist data, expected XML or binary plist\n");
+                } else {
+                    fprintf(stderr, "ERROR: Could not parse plist data (%d)\n", input_res);
+                }
+                ret = 3;
+                break;
+            default:
+                fprintf(stderr, "ERROR: Could not parse plist data (%d)\n", input_res);
+                ret = 1;
+                break;
+        }
     }
 
     free(options);