/*
 * 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 "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 */
}

void plist_ostep_set_debug(int debug)
{
#if DEBUG
    plist_ostep_debug = debug;
#endif
}

#ifndef HAVE_STRNDUP
#ifndef _MSC_VER
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#endif
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;
}
#ifndef _MSC_VER
#pragma GCC diagnostic pop
#endif
#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 plist_err_t 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_INT:
        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);
                }
            }
            plist_err_t 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);
                }
            }
            plist_err_t 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 plist_err_t 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)) {
            plist_err_t 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_INT:
            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_err_t plist_to_openstep(plist_t plist, char **openstep, uint32_t* length, int prettify)
{
    uint64_t size = 0;
    plist_err_t res;

    if (!plist || !openstep || !length) {
        return PLIST_ERR_INVALID_ARG;
    }

    res = node_estimate_size((node_t)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((node_t)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 = (char*)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;
    uint32_t depth;
};
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++;
                }
            }
            if (ctx->pos >= ctx->end) {
                break;
            }
        }
        // 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 plist_err_t 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->end || *ctx->pos == '}') {
            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", (long int)(ctx->pos - ctx->start));
            ctx->err++;
            break;
        }
        parse_skip_ws(ctx);
        if (ctx->pos >= ctx->end) {
            PLIST_OSTEP_ERR("EOF while parsing dictionary '=' delimiter at offset %ld\n", (long int)(ctx->pos - ctx->start));
            ctx->err++;
            break;
        }
        if (*ctx->pos != '=') {
            PLIST_OSTEP_ERR("Missing '=' while parsing dictionary item at offset %ld\n", (long int)(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", (long int)(ctx->pos - ctx->start));
            ctx->err++;
            break;
        }
        val = NULL;
        ctx->err = node_from_openstep(ctx, &val);
        if (ctx->err != 0) {
            break;
        }
        if (!val) {
            PLIST_OSTEP_ERR("Missing value for dictionary item at offset %ld\n", (long int)(ctx->pos - ctx->start));
            ctx->err++;
            break;
        }
        parse_skip_ws(ctx);
        if (ctx->pos >= ctx->end) {
            PLIST_OSTEP_ERR("EOF while parsing dictionary item terminator ';' at offset %ld\n", (long int)(ctx->pos - ctx->start));
            ctx->err++;
            break;
        }
        if (*ctx->pos != ';') {
            PLIST_OSTEP_ERR("Missing terminating ';' while parsing dictionary item at offset %ld\n", (long int)(ctx->pos - ctx->start));
            ctx->err++;
            break;
        }

        plist_dict_set_item(dict, plist_get_string_ptr(key, NULL), val);
        plist_free(key);
        key = NULL;
        val = NULL;

        ctx->pos++;
    }
    plist_free(key);
    plist_free(val);
}

static plist_err_t node_from_openstep(parse_ctx ctx, plist_t *plist)
{
    plist_t subnode = NULL;
    const char *p = NULL;
    ctx->depth++;
    if (ctx->depth > 1000) {
        PLIST_OSTEP_ERR("Too many levels of recursion (%u) at offset %ld\n", ctx->depth, (long int)(ctx->pos - ctx->start));
        ctx->err++;
        return PLIST_ERR_PARSE;
    }
    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 >= ctx->end) {
                PLIST_OSTEP_ERR("EOF while parsing dictionary terminator '}' at offset %ld\n", (long int)(ctx->pos - ctx->start));
                ctx->err++;
                break;
            }
            if (*ctx->pos != '}') {
                PLIST_OSTEP_ERR("Missing terminating '}' at offset %ld\n", (long int)(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 >= ctx->end || *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 >= ctx->end) {
                    PLIST_OSTEP_ERR("EOF while parsing array item delimiter ',' at offset %ld\n", (long int)(ctx->pos - ctx->start));
                    ctx->err++;
                    break;
                }
                if (*ctx->pos != ',') {
                    break;
                }
                ctx->pos++;
            }
	    plist_free(tmp);
	    tmp = NULL;
            if (ctx->err) {
                goto err_out;
            }
            if (ctx->pos >= ctx->end) {
                PLIST_OSTEP_ERR("EOF while parsing array terminator ')' at offset %ld\n", (long int)(ctx->pos - ctx->start));
                ctx->err++;
                break;
            }
            if (*ctx->pos != ')') {
                PLIST_OSTEP_ERR("Missing terminating ')' at offset %ld\n", (long int)(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 >= ctx->end) {
                    PLIST_OSTEP_ERR("EOF while parsing data terminator '>' at offset %ld\n", (long int)(ctx->pos - ctx->start));
                    ctx->err++;
                    break;
                }
                if (*ctx->pos == '>') {
                    break;
                }
                if (!isxdigit(*ctx->pos)) {
                    PLIST_OSTEP_ERR("Invalid byte group in data at offset %ld\n", (long int)(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", (long int)(ctx->pos - ctx->start));
                    ctx->err++;
                    break;
                }
                if (!isxdigit(*ctx->pos)) {
                    PLIST_OSTEP_ERR("Invalid byte group in data at offset %ld\n", (long int)(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) {
                byte_array_free(bytes);
                plist_free_data(data);
                goto err_out;
            }
            if (ctx->pos >= ctx->end) {
                byte_array_free(bytes);
                plist_free_data(data);
                PLIST_OSTEP_ERR("EOF while parsing data terminator '>' at offset %ld\n", (long int)(ctx->pos - ctx->start));
                ctx->err++;
                goto err_out;
            }
            if (*ctx->pos != '>') {
                byte_array_free(bytes);
                plist_free_data(data);
                PLIST_OSTEP_ERR("Missing terminating '>' at offset %ld\n", (long int)(ctx->pos - ctx->start));
                ctx->err++;
                goto err_out;
            }
            ctx->pos++;
            data->buff = (uint8_t*)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 >= ctx->end) {
                plist_free_data(data);
                PLIST_OSTEP_ERR("EOF while parsing quoted string at offset %ld\n", (long int)(ctx->pos - ctx->start));
                ctx->err++;
                goto err_out;
            }
            if (*ctx->pos != c) {
                plist_free_data(data);
                PLIST_OSTEP_ERR("Missing closing quote (%c) at offset %ld\n", c, (long int)(ctx->pos - ctx->start));
                ctx->err++;
                goto err_out;
            }
            size_t slen = ctx->pos - p;
            ctx->pos++; // skip the closing quote
            char* strbuf = (char*)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_free_data(data);
                PLIST_OSTEP_ERR("Unexpected character when parsing unquoted string at offset %ld\n", (long int)(ctx->pos - ctx->start));
                ctx->err++;
                break;
            }
        }
        ctx->pos++;
    }
    ctx->depth--;

err_out:
    if (ctx->err) {
        plist_free(subnode);
        plist_free(*plist);
        *plist = NULL;
        return PLIST_ERR_PARSE;
    }
    return PLIST_ERR_SUCCESS;
}

plist_err_t 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 , 0 };

    plist_err_t 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 = NULL;
            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;
}
