chiark / gitweb /
shared: add minimal JSON tokenizer
[elogind.git] / src / shared / json.c
diff --git a/src/shared/json.c b/src/shared/json.c
new file mode 100644 (file)
index 0000000..f1495e9
--- /dev/null
@@ -0,0 +1,409 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright 2014 Lennart Poettering
+
+  systemd 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.
+
+  systemd 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 systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <sys/types.h>
+#include <math.h>
+
+#include "macro.h"
+#include "log.h"
+#include "util.h"
+#include "utf8.h"
+#include "json.h"
+
+enum {
+        STATE_NULL,
+        STATE_VALUE,
+        STATE_VALUE_POST,
+};
+
+static void inc_lines(unsigned *line, const char *s, size_t n) {
+        const char *p = s;
+
+        if (!line)
+                return;
+
+        for (;;) {
+                const char *f;
+
+                f = memchr(p, '\n', n);
+                if (!f)
+                        return;
+
+                n -= (f - p) + 1;
+                p = f + 1;
+                (*line)++;
+        }
+}
+
+static int json_parse_string(const char **p, char **ret) {
+        _cleanup_free_ char *s = NULL;
+        size_t n = 0, allocated = 0;
+        const char *c;
+
+        assert(p);
+        assert(*p);
+        assert(ret);
+
+        c = *p;
+
+        if (*c != '"')
+                return -EINVAL;
+
+        c++;
+
+        for (;;) {
+                int len;
+
+                /* Check for EOF */
+                if (*c == 0)
+                        return -EINVAL;
+
+                /* Check for control characters 0x00..0x1f */
+                if (*c > 0 && *c < ' ')
+                        return -EINVAL;
+
+                /* Check for control character 0x7f */
+                if (*c == 0x7f)
+                        return -EINVAL;
+
+                if (*c == '"') {
+                        if (!s) {
+                                s = strdup("");
+                                if (!s)
+                                        return -ENOMEM;
+                        } else
+                                s[n] = 0;
+
+                        *p = c + 1;
+
+                        *ret = s;
+                        s = NULL;
+                        return JSON_STRING;
+                }
+
+                if (*c == '\\') {
+                        char ch = 0;
+                        c++;
+
+                        if (*c == 0)
+                                return -EINVAL;
+
+                        if (IN_SET(*c, '"', '\\', '/'))
+                                ch = *c;
+                        else if (*c == 'b')
+                                ch = '\b';
+                        else if (*c == 'f')
+                                ch = '\f';
+                        else if (*c == 'n')
+                                ch = '\n';
+                        else if (*c == 'r')
+                                ch = '\r';
+                        else if (*c == 't')
+                                ch = '\t';
+                        else if (*c == 'u') {
+                                int aa, bb, cc, dd;
+                                uint16_t x;
+
+                                aa = unhexchar(c[1]);
+                                if (aa < 0)
+                                        return -EINVAL;
+
+                                bb = unhexchar(c[2]);
+                                if (bb < 0)
+                                        return -EINVAL;
+
+                                cc = unhexchar(c[3]);
+                                if (cc < 0)
+                                        return -EINVAL;
+
+                                dd = unhexchar(c[4]);
+                                if (dd < 0)
+                                        return -EINVAL;
+
+
+                                x =     ((uint16_t) aa << 12) |
+                                        ((uint16_t) bb << 8) |
+                                        ((uint16_t) cc << 4) |
+                                        ((uint16_t) dd);
+
+                                if (x <= 0)
+                                        return -EINVAL;
+
+                                if (!GREEDY_REALLOC(s, allocated, n + 4))
+                                        return -ENOMEM;
+
+                                n += utf8_encode_unichar(x, s + n);
+                                c += 5;
+                                continue;
+                        } else
+                                return -EINVAL;
+
+                        if (!GREEDY_REALLOC(s, allocated, n + 2))
+                                return -ENOMEM;
+
+                        s[n++] = ch;
+                        c ++;
+                        continue;
+                }
+
+                len = utf8_encoded_valid_unichar(c);
+                if (len < 0)
+                        return len;
+
+                if (!GREEDY_REALLOC(s, allocated, n + len + 1))
+                        return -ENOMEM;
+
+                memcpy(s + n, c, len);
+                n += len;
+                c += len;
+        }
+}
+
+static int json_parse_number(const char **p, union json_value *ret) {
+        bool negative = false, exponent_negative = false, is_double = false;
+        double x = 0.0, y = 0.0, exponent = 0.0, shift = 1.0;
+        intmax_t i = 0;
+        const char *c;
+
+        assert(p);
+        assert(*p);
+        assert(ret);
+
+        c = *p;
+
+        if (*c == '-') {
+                negative = true;
+                c++;
+        }
+
+        if (*c == '0')
+                c++;
+        else {
+                if (!strchr("123456789", *c) || *c == 0)
+                        return -EINVAL;
+
+                do {
+                        if (!is_double) {
+                                int64_t t;
+
+                                t = 10 * i + (*c - '0');
+                                if (t < i) /* overflow */
+                                        is_double = false;
+                                else
+                                        i = t;
+                        }
+
+                        x = 10.0 * x + (*c - '0');
+                        c++;
+                } while (strchr("0123456789", *c) && *c != 0);
+        }
+
+        if (*c == '.') {
+                is_double = true;
+                c++;
+
+                if (!strchr("0123456789", *c) || *c == 0)
+                        return -EINVAL;
+
+                do {
+                        y = 10.0 * y + (*c - '0');
+                        shift = 10.0 * shift;
+                        c++;
+                } while (strchr("0123456789", *c) && *c != 0);
+        }
+
+        if (*c == 'e' || *c == 'E') {
+                is_double = true;
+                c++;
+
+                if (*c == '-') {
+                        exponent_negative = true;
+                        c++;
+                } else if (*c == '+')
+                        c++;
+
+                if (!strchr("0123456789", *c) || *c == 0)
+                        return -EINVAL;
+
+                do {
+                        exponent = 10.0 * exponent + (*c - '0');
+                        c++;
+                } while (strchr("0123456789", *c) && *c != 0);
+        }
+
+        if (*c != 0)
+                return -EINVAL;
+
+        *p = c;
+
+        if (is_double) {
+                ret->real = ((negative ? -1.0 : 1.0) * (x + (y / shift))) * exp10((exponent_negative ? -1.0 : 1.0) * exponent);
+                return JSON_REAL;
+        } else {
+                ret->integer = negative ? -i : i;
+                return JSON_INTEGER;
+        }
+}
+
+int json_tokenize(
+                const char **p,
+                char **ret_string,
+                union json_value *ret_value,
+                void **state,
+                unsigned *line) {
+
+        const char *c;
+        int t;
+        int r;
+
+        assert(p);
+        assert(*p);
+        assert(ret_string);
+        assert(ret_value);
+        assert(state);
+
+        t = PTR_TO_INT(*state);
+        c = *p;
+
+        if (t == STATE_NULL) {
+                if (line)
+                        *line = 1;
+                t = STATE_VALUE;
+        }
+
+        for (;;) {
+                const char *b;
+
+                b = c + strspn(c, WHITESPACE);
+                if (*b == 0)
+                        return JSON_END;
+
+                inc_lines(line, c, b - c);
+                c = b;
+
+                switch (t) {
+
+                case STATE_VALUE:
+
+                        if (*c == '{') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE);
+                                return JSON_OBJECT_OPEN;
+
+                        } else if (*c == '}') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return JSON_OBJECT_CLOSE;
+
+                        } else if (*c == '[') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE);
+                                return JSON_ARRAY_OPEN;
+
+                        } else if (*c == ']') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return JSON_ARRAY_CLOSE;
+
+                        } else if (*c == '"') {
+                                r = json_parse_string(&c, ret_string);
+                                if (r < 0)
+                                        return r;
+
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return r;
+
+                        } else if (strchr("-0123456789", *c)) {
+                                r = json_parse_number(&c, ret_value);
+                                if (r < 0)
+                                        return r;
+
+                                *ret_string = NULL;
+                                *p = c;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return r;
+
+                        } else if (startswith(c, "true")) {
+                                *ret_string = NULL;
+                                ret_value->boolean = true;
+                                *p = c + 4;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return JSON_BOOLEAN;
+
+                        } else if (startswith(c, "false")) {
+                                *ret_string = NULL;
+                                ret_value->boolean = false;
+                                *p = c + 5;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return JSON_BOOLEAN;
+
+                        } else if (startswith(c, "null")) {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 4;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return JSON_NULL;
+
+                        } else
+                                return -EINVAL;
+
+                case STATE_VALUE_POST:
+
+                        if (*c == ':') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE);
+                                return JSON_COLON;
+                        } else if (*c == ',') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE);
+                                return JSON_COMMA;
+                        } else if (*c == '}') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return JSON_OBJECT_CLOSE;
+                        } else if (*c == ']') {
+                                *ret_string = NULL;
+                                *ret_value = JSON_VALUE_NULL;
+                                *p = c + 1;
+                                *state = INT_TO_PTR(STATE_VALUE_POST);
+                                return JSON_ARRAY_CLOSE;
+                        } else
+                                return -EINVAL;
+                }
+
+        }
+}