lpc-json/
/**
 * json.c
 *
 * LPC support functions for JSON serialization and deserialization.
 * Attempts to be compatible with reasonably current FluffOS and LDMud
 * drivers, with at least a gesture or two toward compatibility with
 * older drivers.
 *
 * Web site for updates: http://lostsouls.org/grimoire_json
 *
 * mixed json_decode(string text)
 *     Deserializes JSON into an LPC value.
 *
 * string json_encode(mixed value)
 *     Serializes an LPC value into JSON text.
 *
 * v1.0: initial release
 * v1.0.1: fix for handling of \uXXXX on MudOS
 * v1.0.2: define array keyword for LDMud & use it consistently
 * v1.0.3: fix for empty data structures
 *
 * LICENSE
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Thaumatic Systems, LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#ifdef MUDOS
#define strstr                      strsrch
#define raise_error                 error
#define member(x, y)                member_array(y, x)
#define to_string(x)                ("" + (x))
#else // MUDOS
#include <lpctypes.h>
#define replace_string(x, y, z)     implode(explode((x), (y)), (z))
#define in                          :
#ifndef array
#define array                       *
#endif // !array
#endif // MUDOS

#define JSON_DECODE_PARSE_TEXT      0
#define JSON_DECODE_PARSE_POS       1
#define JSON_DECODE_PARSE_LINE      2
#define JSON_DECODE_PARSE_CHAR      3

#define JSON_DECODE_PARSE_FIELDS    4

private mixed json_decode_parse_value(mixed array parse);
private varargs mixed json_decode_parse_string(mixed array parse, int initiator_checked);

private void json_decode_parse_next_char(mixed array parse) {
    parse[JSON_DECODE_PARSE_POS]++;
    parse[JSON_DECODE_PARSE_CHAR]++;
}

private void json_decode_parse_next_chars(mixed array parse, int num) {
    parse[JSON_DECODE_PARSE_POS] += num;
    parse[JSON_DECODE_PARSE_CHAR] += num;
}

private void json_decode_parse_next_line(mixed array parse) {
    parse[JSON_DECODE_PARSE_POS]++;
    parse[JSON_DECODE_PARSE_LINE]++;
    parse[JSON_DECODE_PARSE_CHAR] = 1;
}

private int json_decode_hexdigit(int ch) {
    switch(ch) {
    case '0'    :
        return 0;
    case '1'    :
    case '2'    :
    case '3'    :
    case '4'    :
    case '5'    :
    case '6'    :
    case '7'    :
    case '8'    :
    case '9'    :
        return ch - '0';
    case 'a'    :
    case 'A'    :
        return 10;
    case 'b'    :
    case 'B'    :
        return 11;
    case 'c'    :
    case 'C'    :
        return 12;
    case 'd'    :
    case 'D'    :
        return 13;
    case 'e'    :
    case 'E'    :
        return 14;
    case 'f'    :
    case 'F'    :
        return 15;
    }
    return -1;
}

private varargs int json_decode_parse_at_token(mixed array parse, string token, int start) {
    int i, j;
    for(i = start, j = strlen(token); i < j; i++)
        if(parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS] + i] != token[i])
            return 0;
    return 1;
}

private varargs void json_decode_parse_error(mixed array parse, string msg, int ch) {
    if(ch)
        msg = sprintf("%s, '%c'", msg, ch);
    msg = sprintf("%s @ line %d char %d\n", msg, parse[JSON_DECODE_PARSE_LINE], parse[JSON_DECODE_PARSE_CHAR]);
    raise_error(msg);
}

private mixed json_decode_parse_object(mixed array parse) {
    mapping out = ([]);
    int done = 0;
    mixed key, value;
    int found_non_whitespace, found_sep, found_comma;
    json_decode_parse_next_char(parse);
    if(parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]] == '}') {
        done = 1;
        json_decode_parse_next_char(parse);
    }
    while(!done) {
        found_non_whitespace = 0;
        while(!found_non_whitespace) {
            switch(parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]]) {
            case 0      :
                json_decode_parse_error(parse, "Unexpected end of data");
            case ' '    :
            case '\t'   :
            case '\r'   :
                json_decode_parse_next_char(parse);
                break;
#ifdef MUDOS
            case 0x0c   :
#else // MUDOS
            case '\f'   :
#endif // MUDOS
            case '\n'   :
                json_decode_parse_next_line(parse);
                break;
            default     :
                found_non_whitespace = 1;
                break;
            }
        }
        key = json_decode_parse_string(parse);
        found_sep = 0;
        while(!found_sep) {
            int ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
            switch(ch) {
            case 0      :
                json_decode_parse_error(parse, "Unexpected end of data");
            case ':'    :
                found_sep = 1;
                json_decode_parse_next_char(parse);
                break;
            case ' '    :
            case '\t'   :
            case '\r'   :
                json_decode_parse_next_char(parse);
                break;
#ifdef MUDOS
            case 0x0c   :
#else // MUDOS
            case '\f'   :
#endif // MUDOS
            case '\n'   :
                json_decode_parse_next_line(parse);
                break;
            default     :
                json_decode_parse_error(parse, "Unexpected character", ch);
            }
        }
        value = json_decode_parse_value(parse);
        found_comma = 0;
        while(!found_comma && !done) {
            int ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
            switch(ch) {
            case 0      :
                json_decode_parse_error(parse, "Unexpected end of data");
            case ','    :
                found_comma = 1;
                json_decode_parse_next_char(parse);
                break;
            case '}'    :
                done = 1;
                json_decode_parse_next_char(parse);
                break;
            case ' '    :
            case '\t'   :
            case '\r'   :
                json_decode_parse_next_char(parse);
                break;
#ifdef MUDOS
            case 0x0c   :
#else // MUDOS
            case '\f'   :
#endif // MUDOS
            case '\n'   :
                json_decode_parse_next_line(parse);
                break;
            default     :
                json_decode_parse_error(parse, "Unexpected character", ch);
            }
        }
        out[key] = value;
    }
    return out;
}

private mixed json_decode_parse_array(mixed array parse) {
    mixed array out = ({});
    int done = 0;
    int found_comma;
    json_decode_parse_next_char(parse);
    if(parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]] == ']') {
        done = 1;
        json_decode_parse_next_char(parse);
    }
    while(!done) {
        mixed value = json_decode_parse_value(parse);
        found_comma = 0;
        while(!found_comma && !done) {
            int ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
            switch(ch) {
            case 0      :
                json_decode_parse_error(parse, "Unexpected end of data");
            case ','    :
                found_comma = 1;
                json_decode_parse_next_char(parse);
                break;
            case ']'    :
                done = 1;
                json_decode_parse_next_char(parse);
                break;
            case ' '    :
            case '\t'   :
            case '\r'   :
                json_decode_parse_next_char(parse);
                break;
#ifdef MUDOS
            case 0x0c   :
#else // MUDOS
            case '\f'   :
#endif // MUDOS
            case '\n'   :
                json_decode_parse_next_line(parse);
                break;
            default     :
                json_decode_parse_error(parse, "Unexpected character", ch);
            }
        }
        out += ({ value });
    }
    return out;
}

private varargs mixed json_decode_parse_string(mixed array parse, int initiator_checked) {
    int from, to, esc_state, esc_active;
    string out;
    if(!initiator_checked) {
        int ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
        if(!ch)
            json_decode_parse_error(parse, "Unexpected end of data");
        if(ch != '"')
            json_decode_parse_error(parse, "Unexpected character", ch);
    }
    json_decode_parse_next_char(parse);
    from = parse[JSON_DECODE_PARSE_POS];
    to = -1;
    esc_state = 0;
    esc_active = 0;
    while(to == -1) {
        switch(parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]]) {
        case 0          :
            json_decode_parse_error(parse, "Unexpected end of data");
        case '\\'       :
            esc_state = !esc_state;
            break;
        case '"'        :
            if(esc_state) {
                esc_state = 0;
                esc_active++;
            } else {
                to = parse[JSON_DECODE_PARSE_POS] - 1;
            }
            break;
        default         :
            if(esc_state) {
                esc_state = 0;
                esc_active++;
            }
            break;
        }
        json_decode_parse_next_char(parse);
    }
    out = parse[JSON_DECODE_PARSE_TEXT][from .. to];
    if(esc_active) {
        if(member(out, '"') != -1)
            out = replace_string(out, "\\\"", "\"");
        if(strstr(out, "\\b") != -1)
            out = replace_string(out, "\\b", "\b");
#ifdef MUDOS
        if(strstr(out, "\\f") != -1)
            out = replace_string(out, "\\f", "\x0c");
#else // MUDOS
        if(strstr(out, "\\f") != -1)
            out = replace_string(out, "\\f", "\f");
#endif // MUDOS
        if(strstr(out, "\\n") != -1)
            out = replace_string(out, "\\n", "\n");
        if(strstr(out, "\\r") != -1)
            out = replace_string(out, "\\r", "\r");
        if(strstr(out, "\\t") != -1)
            out = replace_string(out, "\\t", "\t");
        if(strstr(out, "\\u") != -1) {
            string array parts = explode(out, "\\u");
            int entries = sizeof(parts) * 2 - 1;
            string array proc = allocate(entries);
            int i, j;
            proc[0] = parts[0];
            for(i = 1, j = sizeof(parts); i < j; i++) {
                string part = parts[i];
                int array nybbles = allocate(4);
                int array bytes = allocate(2);
                for(int k = 0; k < 4; k++)
                    if((nybbles[k] = json_decode_hexdigit(part[k])) == -1)
                        json_decode_parse_error(parse, "Invalid hex digit", part[k]);
                bytes[0] = (nybbles[0] << 4) | nybbles[1];
                bytes[1] = (nybbles[2] << 4) | nybbles[3];
                if(member(bytes, 0) != -1)
                    bytes -= ({ 0 });
#ifdef MUDOS
                proc[i * 2 - 1] = sprintf("%@c", bytes);
#else // MUDOS
                proc[i * 2 - 1] = to_string(bytes);
#endif // MUDOS
                proc[i * 2] = part[4..];
            }
            out = implode(proc, "");
        }
        if(member(out, '/') != -1)
            out = replace_string(out, "\\/", "/");
        if(member(out, '\\') != -1)
            out = replace_string(out, "\\\\", "\\");
    }
    return out;
}

private mixed json_decode_parse_number(mixed array parse) {
    int from = parse[JSON_DECODE_PARSE_POS];
    int to = -1;
    int dot = -1;
    int exp = -1;
    int first_ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
    int ch;
    string number;
    switch(first_ch) {
    case '-'            :
        {
            int next_ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS] + 1];
            if(next_ch != '0')
                break;
            json_decode_parse_next_char(parse);
        }
        // Fallthrough
    case '0'            :
        json_decode_parse_next_char(parse);
        ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
        if(ch) {
            if(ch != '.')
                json_decode_parse_error(parse, "Unexpected character", ch);
            dot = parse[JSON_DECODE_PARSE_POS];
        }
        break;
    }
    json_decode_parse_next_char(parse);
    while(to == -1) {
        ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
        switch(ch) {
        case '.'        :
            if(dot != -1 || exp != -1)
                json_decode_parse_error(parse, "Unexpected character", ch);
            dot = parse[JSON_DECODE_PARSE_POS];
            json_decode_parse_next_char(parse);
            break;
        case '0'        :
        case '1'        :
        case '2'        :
        case '3'        :
        case '4'        :
        case '5'        :
        case '6'        :
        case '7'        :
        case '8'        :
        case '9'        :
            json_decode_parse_next_char(parse);
            break;
        case 'e'        :
        case 'E'        :
            if(exp != -1)
                json_decode_parse_error(parse, "Unexpected character", ch);
            exp = parse[JSON_DECODE_PARSE_POS];
            json_decode_parse_next_char(parse);
            break;
        case '-'        :
        case '+'        :
            if(exp == parse[JSON_DECODE_PARSE_POS] - 1) {
                json_decode_parse_next_char(parse);
                break;
            }
            // Fallthrough
        default         :
            to = parse[JSON_DECODE_PARSE_POS] - 1;
            if(dot == to || to < from)
                json_decode_parse_error(parse, "Unexpected character", ch);
            break;
        }
    }
    number = parse[JSON_DECODE_PARSE_TEXT][from .. to];
    if(dot != -1 || exp != -1)
        return to_float(number);
    else
        return to_int(number);
}

private mixed json_decode_parse_value(mixed array parse) {
    for(;;) {
        int ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
        switch(ch) {
        case 0          :
            json_decode_parse_error(parse, "Unexpected end of data");
        case '{'        :
            return json_decode_parse_object(parse);
        case '['        :
            return json_decode_parse_array(parse);
        case '"'        :
            return json_decode_parse_string(parse, 1);
        case '-'        :
        case '0'        :
        case '1'        :
        case '2'        :
        case '3'        :
        case '4'        :
        case '5'        :
        case '6'        :
        case '7'        :
        case '8'        :
        case '9'        :
            return json_decode_parse_number(parse);
        case ' '        :
        case '\t'       :
        case '\r'       :
            json_decode_parse_next_char(parse);
            break;
#ifdef MUDOS
        case 0x0c       :
#else // MUDOS
        case '\f'       :
#endif // MUDOS
        case '\n'       :
            json_decode_parse_next_line(parse);
            break;
        case 't'        :
            if(json_decode_parse_at_token(parse, "true", 1)) {
                json_decode_parse_next_chars(parse, 4);
                return 1;
            } else {
                json_decode_parse_error(parse, "Unexpected character", ch);
            }
        case 'f'        :
            if(json_decode_parse_at_token(parse, "false", 1)) {
                json_decode_parse_next_chars(parse, 5);
                return 0;
            } else {
                json_decode_parse_error(parse, "Unexpected character", ch);
            }
        case 'n'        :
            if(json_decode_parse_at_token(parse, "null", 1)) {
                json_decode_parse_next_chars(parse, 4);
                return 0;
            } else {
                json_decode_parse_error(parse, "Unexpected character", ch);
            }
        default         :
            json_decode_parse_error(parse, "Unexpected character", ch);
        }
    }
}

private mixed json_decode_parse(mixed array parse) {
    mixed out = json_decode_parse_value(parse);
    for(;;) {
        int ch = parse[JSON_DECODE_PARSE_TEXT][parse[JSON_DECODE_PARSE_POS]];
        switch(ch) {
        case 0          :
            return out;
        case ' '        :
        case '\t'       :
        case '\r'       :
            json_decode_parse_next_char(parse);
            break;
#ifdef MUDOS
        case 0x0c       :
#else // MUDOS
        case '\f'       :
#endif // MUDOS
        case '\n'       :
            json_decode_parse_next_line(parse);
            break;
        default         :
            json_decode_parse_error(parse, "Unexpected character", ch);
        }
    }
    return 0;
}

mixed json_decode(string text) {
    mixed array parse = allocate(JSON_DECODE_PARSE_FIELDS);
    parse[JSON_DECODE_PARSE_TEXT] = text;
    parse[JSON_DECODE_PARSE_POS] = 0;
    parse[JSON_DECODE_PARSE_CHAR] = 1;
    parse[JSON_DECODE_PARSE_LINE] = 1;
    return json_decode_parse(parse);
}

varargs string json_encode(mixed value, mixed array pointers) {
#ifdef MUDOS
    if(undefinedp(value))
        return "null";
    if(intp(value) || floatp(value))
#else // MUDOS
    switch(typeof(value)) {
    case T_NUMBER   :
    case T_FLOAT    :
#endif // MUDOS
        return to_string(value);
#ifdef MUDOS
    if(stringp(value)) {
#else // MUDOS
    case T_STRING   :
#endif // MUDOS
        if(member(value, '"') != -1)
            value = replace_string(value, "\"", "\\\"");
        value = sprintf("\"%s\"", value);
        if(member(value, '\\') != -1) {
            value = replace_string(value, "\\", "\\\\");
            if(strstr(value, "\\\"") != -1)
                value = replace_string(value, "\\\"", "\"");
        }
        if(member(value, '\b') != -1)
            value = replace_string(value, "\b", "\\b");
#ifdef MUDOS
        if(member(value, 0x0c) != -1)
            value = replace_string(value, "\x0c", "\\f");
#else // MUDOS
        if(member(value, '\f') != -1)
            value = replace_string(value, "\f", "\\f");
#endif // MUDOS
        if(member(value, '\n') != -1)
            value = replace_string(value, "\n", "\\n");
        if(member(value, '\r') != -1)
            value = replace_string(value, "\r", "\\r");
        if(member(value, '\t') != -1)
            value = replace_string(value, "\t", "\\t");
        return value;
#ifdef MUDOS
    }
    if(mapp(value)) {
        string out;
        int ix = 0;
        if(pointers) {
            // Don't recurse into circular data structures, output null for
            // their interior reference
            if(member(pointers, value) != -1)
                return "null";
            pointers += ({ value });
        } else {
            pointers = ({ value });
        }
        foreach(mixed k, mixed v in value) {
            // Non-string keys are skipped because the JSON spec requires that
            // object field names be strings.
            if(!stringp(k))
                continue;
            if(ix++)
                out = sprintf("%s,%s:%s", out, json_encode(k, pointers), json_encode(v, pointers));
            else
                out = sprintf("%s:%s", json_encode(k, pointers), json_encode(v, pointers));
        }
        return sprintf("{%s}", out);
    }
#else // MUDOS
    case T_MAPPING  :
        if(sizeof(value)) {
#ifdef __LDMUD__
            int width = widthof(value);
#else // __LDMUD__
            int width = get_type_info(value, 1);
#endif // __LDMUD__
            switch(width) {
            case 1      :
                {
                    string out;
                    int ix = 0;
                    if(pointers) {
                        // Don't recurse into circular data structures, output null for
                        // their interior reference
                        if(member(pointers, value) != -1)
                            return "null";
                        pointers += ({ value });
                    } else {
                        pointers = ({ value });
                    }
                    foreach(mixed k, mixed v : value) {
                        // Non-string keys are skipped because the JSON spec requires that
                        // object field names be strings.
                        if(!stringp(k))
                            continue;
                        if(ix++)
                            out = sprintf("%s,%s:%s", out, json_encode(k, pointers), json_encode(v, pointers));
                        else
                            out = sprintf("%s:%s", json_encode(k, pointers), json_encode(v, pointers));
                    }
                    return sprintf("{%s}", out);
                }
            default     :
                // Multivalue mappings are represented using arrays for the value sets.
                // This isn't a reversible representation, so it's fairly problematic, but
                // it seems marginally better than just dropping the values on the floor.
                {
                    mixed array values;
                    string out;
                    int ix = 0;
                    if(pointers) {
                        // Don't recurse into circular data structures, output null for
                        // their interior reference
                        if(member(pointers, value) != -1)
                            return "null";
                        pointers += ({ value });
                    } else {
                        pointers = ({ value });
                    }
                    values = allocate(width);
                    foreach(mixed k, mixed v1, mixed v2 : value) {
                        // Non-string keys are skipped because the JSON spec requires that
                        // object field names be strings.
                        if(!stringp(k))
                            continue;
                        values[0] = v1;
                        values[1] = v2;
                        for(int w = 2; w < width; w++)
                            values[w] = value[k, w];
                        if(ix++)
                            out = sprintf("%s,%s:%s", out, json_encode(k, pointers), json_encode(values, pointers));
                        else
                            out = sprintf("%s:%s", json_encode(k, pointers), json_encode(values, pointers));
                    }
                    return sprintf("{%s}", out);
                }
            case 0      :
                break;
            }
        } else {
	        return "{}";
		}
#endif // MUDOS
#ifdef MUDOS
    if(arrayp(value))
#else // MUDOS
    case T_POINTER  :
#endif // MUDOS
        if(sizeof(value)) {
            string out;
            int ix = 0;
            if(pointers) {
                // Don't recurse into circular data structures, output null for
                // their interior reference
                if(member(pointers, value) != -1)
                    return "null";
                pointers += ({ value });
            } else {
                pointers = ({ value });
            }
            foreach(mixed v in value)
                if(ix++)
                    out = sprintf("%s,%s", out, json_encode(v, pointers));
                else
                    out = json_encode(v, pointers);
            return sprintf("[%s]", out);
        } else {
	        return "[]";
		}
#ifndef MUDOS
    }
#endif // !MUDOS
    // Values that cannot be represented in JSON are replaced by nulls.
    return "null";
}