initial repo - jfconvert - JSON Feed (subset) to sfeed or Atom converter
 (HTM) git clone git://git.codemadness.org/jfconvert
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit f7cde52eef12a6e77c28199a678de8665836e9e6
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Mon,  3 Apr 2023 18:20:19 +0200
       
       initial repo
       
       Diffstat:
         A LICENSE                             |      15 +++++++++++++++
         A Makefile                            |      88 +++++++++++++++++++++++++++++++
         A README                              |      46 +++++++++++++++++++++++++++++++
         A jf2atom.1                           |      39 +++++++++++++++++++++++++++++++
         A jf2atom.c                           |     267 +++++++++++++++++++++++++++++++
         A json.c                              |     319 +++++++++++++++++++++++++++++++
         A json.h                              |      30 ++++++++++++++++++++++++++++++
       
       7 files changed, 804 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/LICENSE b/LICENSE
       @@ -0,0 +1,15 @@
       +ISC License
       +
       +Copyright (c) 2023 Hiltjo Posthuma <hiltjo@codemadness.org>
       +
       +Permission to use, copy, modify, and/or distribute this software for any
       +purpose with or without fee is hereby granted, provided that the above
       +copyright notice and this permission notice appear in all copies.
       +
       +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 (DIR) diff --git a/Makefile b/Makefile
       @@ -0,0 +1,88 @@
       +.POSIX:
       +
       +NAME = jf2atom
       +VERSION = 0.1
       +
       +# paths
       +PREFIX = /usr/local
       +MANPREFIX = ${PREFIX}/man
       +DOCPREFIX = ${PREFIX}/share/doc/${NAME}
       +
       +RANLIB = ranlib
       +
       +# use system flags.
       +JFA_CFLAGS = ${CFLAGS}
       +JFA_LDFLAGS = ${LDFLAGS}
       +JFA_CPPFLAGS = -D_DEFAULT_SOURCE
       +
       +# uncomment for conservative locked I/O.
       +#JFA_CPPFLAGS = -D_DEFAULT_SOURCE -DGETNEXT=getchar
       +
       +BIN = ${NAME}
       +SRC = ${BIN:=.c}
       +HDR = json.h
       +MAN1 = ${BIN:=.1}
       +DOC = \
       +        LICENSE\
       +        README
       +
       +LIBJSON = libjson.a
       +LIBJSONSRC = json.c
       +LIBJSONOBJ = ${LIBJSONSRC:.c=.o}
       +
       +LIB = ${LIBJSON}
       +
       +all: ${BIN}
       +
       +${BIN}: ${LIB} ${@:=.o}
       +
       +OBJ = ${SRC:.c=.o} ${LIBJSONOBJ}
       +
       +${OBJ}: ${HDR}
       +
       +.o:
       +        ${CC} ${JFA_LDFLAGS} -o $@ $< ${LIB}
       +
       +.c.o:
       +        ${CC} ${JFA_CFLAGS} ${JFA_CPPFLAGS} -o $@ -c $<
       +
       +${LIBJSON}: ${LIBJSONOBJ}
       +        ${AR} -rc $@ $?
       +        ${RANLIB} $@
       +
       +dist:
       +        rm -rf "${NAME}-${VERSION}"
       +        mkdir -p "${NAME}-${VERSION}"
       +        cp -f ${MAN1} ${DOC} ${HDR} \
       +                ${SRC} ${LIBJSONSRC} Makefile "${NAME}-${VERSION}"
       +        # make tarball
       +        tar cf - "${NAME}-${VERSION}" | gzip -c > "${NAME}-${VERSION}.tar.gz"
       +        rm -rf "${NAME}-${VERSION}"
       +
       +clean:
       +        rm -f ${BIN} ${OBJ} ${LIB}
       +
       +install: all
       +        # installing executable files.
       +        mkdir -p "${DESTDIR}${PREFIX}/bin"
       +        cp -f ${BIN} "${DESTDIR}${PREFIX}/bin"
       +        for f in ${BIN}; do chmod 755 "${DESTDIR}${PREFIX}/bin/$$f"; done
       +        # installing example files.
       +        mkdir -p "${DESTDIR}${DOCPREFIX}"
       +        cp -f ${DOC} "${DESTDIR}${DOCPREFIX}"
       +        for d in ${DOC}; do chmod 644 "${DESTDIR}${DOCPREFIX}/$$d"; done
       +        # installing manual pages for general commands: section 1.
       +        mkdir -p "${DESTDIR}${MANPREFIX}/man1"
       +        cp -f ${MAN1} "${DESTDIR}${MANPREFIX}/man1"
       +        for m in ${MAN1}; do chmod 644 "${DESTDIR}${MANPREFIX}/man1/$$m"; done
       +
       +uninstall:
       +        # removing executable files.
       +        for f in ${BIN}; do rm -f "${DESTDIR}${PREFIX}/bin/$$f"; done
       +        # removing example files.
       +        for d in ${DOC}; do rm -f "${DESTDIR}${DOCPREFIX}/$$d"; done
       +        -rmdir "${DESTDIR}${DOCPREFIX}"
       +        # removing manual pages.
       +        for m in ${MAN1}; do rm -f "${DESTDIR}${MANPREFIX}/man1/$$m"; done
       +
       +.PHONY: all clean dist install uninstall
 (DIR) diff --git a/README b/README
       @@ -0,0 +1,46 @@
       +jf2atom
       +-------
       +
       +JSON Feed (subset) to Atom converter.
       +
       +JSON Feed specification: https://www.jsonfeed.org/version/1/
       +Atom specification:      https://datatracker.ietf.org/doc/html/rfc4287
       +
       +
       +Build and install
       +-----------------
       +
       +$ make
       +# make install
       +
       +
       +Dependencies
       +------------
       +
       +- C compiler (C99).
       +- libc
       +
       +
       +Optional dependencies
       +---------------------
       +
       +- POSIX make(1) (for Makefile).
       +- mandoc for documentation: https://mdocml.bsd.lv/
       +
       +
       +Examples and documentation
       +--------------------------
       +
       +See the man page.
       +
       +
       +License
       +-------
       +
       +ISC, see LICENSE file.
       +
       +
       +Author
       +------
       +
       +Hiltjo Posthuma <hiltjo@codemadness.org>
 (DIR) diff --git a/jf2atom.1 b/jf2atom.1
       @@ -0,0 +1,39 @@
       +.Dd April 3, 2023
       +.Dt JF2ATOM 1
       +.Os
       +.Sh NAME
       +.Nm jf2atom
       +.Nd convert JSON Feed to Atom
       +.Sh SYNOPSIS
       +.Nm
       +.Sh DESCRIPTION
       +.Nm
       +reads JSON data from stdin.
       +It writes an Atom feed to stdout.
       +.Sh EXIT STATUS
       +.Ex -std
       +.Sh EXAMPLES
       +.Bd -literal
       +jf2atom < input.json
       +.Ed
       +.Pp
       +An example to support JSON Feed in a RSS/Atom reader:
       +.Bd -literal
       +curl -s 'https://codemadness.org/jsonfeed_content.json' | jf2atom | sfeed | sfeed_curses
       +.Ed
       +.Sh SEE ALSO
       +.Xr awk 1 ,
       +.Xr curl 1 ,
       +.Xr sfeed 1
       +.Sh STANDARDS
       +.Rs
       +.%T The Atom Syndication Format
       +.%R RFC 4287
       +.Re
       +.Rs
       +.%T JSON Feed Version 1.1
       +.%U https://www.jsonfeed.org/version/1.1/
       +.%D Nov, 2022
       +.Re
       +.Sh AUTHORS
       +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
 (DIR) diff --git a/jf2atom.c b/jf2atom.c
       @@ -0,0 +1,267 @@
       +#include <errno.h>
       +#include <limits.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#ifdef __OpenBSD__
       +#include <unistd.h>
       +#else
       +#define pledge(a,b) 0
       +#endif
       +
       +#include "json.h"
       +
       +/* control-character in the ASCII range 0-127: compatible with UTF-8 */
       +#define ISCNTRL(c) ((c) < ' ' || (c) == 0x7f)
       +
       +static int itemisopen = 0, enclosureisopen = 0;
       +
       +/* Escape characters below as HTML 2.0 / XML 1.0. */
       +void
       +xmlencode(const char *s, FILE *fp)
       +{
       +        for (; *s; ++s) {
       +                switch (*s) {
       +                case '<':  fputs("&lt;",   fp); break;
       +                case '>':  fputs("&gt;",   fp); break;
       +                case '\'': fputs("&#39;",  fp); break;
       +                case '&':  fputs("&amp;",  fp); break;
       +                case '"':  fputs("&quot;", fp); break;
       +                default:   putc(*s, fp);
       +                }
       +        }
       +}
       +
       +void
       +processnode(struct json_node *nodes, size_t depth, const char *value)
       +{
       +        const char *outtag, *outtype, *outhref;
       +
       +        /* feed / channel */
       +        if (depth == 2) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT) {
       +                        if (nodes[1].type == JSON_TYPE_STRING) {
       +                                if (!strcasecmp(nodes[1].name, "title")) {
       +                                        fputs("<title type=\"text\">", stdout);
       +                                        xmlencode(value, stdout);
       +                                        fputs("</title>\n", stdout);
       +                                } else if (!strcasecmp(nodes[1].name, "home_page_url")) {
       +                                        fputs("<link rel=\"alternate\" type=\"text/html\" href=\"", stdout);
       +                                        xmlencode(value, stdout);
       +                                        fputs("\" />\n", stdout);
       +                                } else if (!strcasecmp(nodes[1].name, "description")) {
       +                                        fputs("<subtitle>", stdout);
       +                                        xmlencode(value, stdout);
       +                                        fputs("</subtitle>\n", stdout);
       +                                }
       +                        }
       +                }
       +        }
       +
       +        /* item */
       +        if (depth == 3) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    nodes[1].type == JSON_TYPE_ARRAY &&
       +                    nodes[2].type == JSON_TYPE_OBJECT &&
       +                    !strcasecmp(nodes[1].name, "items")) {
       +                        if (enclosureisopen) {
       +                                fputs(" />\n", stdout);
       +                                enclosureisopen = 0;
       +                        }
       +                        if (itemisopen)
       +                                fputs("</entry>\n", stdout);
       +                        fputs("<entry>\n", stdout);
       +                        itemisopen = 1;
       +                }
       +        }
       +
       +        /* item attributes */
       +        if (depth == 4) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    nodes[1].type == JSON_TYPE_ARRAY &&
       +                    nodes[2].type == JSON_TYPE_OBJECT &&
       +                    !strcasecmp(nodes[1].name, "items")) {
       +                        outtag = NULL;
       +                        outtype = NULL;
       +                        outhref = NULL;
       +
       +                        if (!strcasecmp(nodes[3].name, "content_html")) {
       +                                outtag = "content";
       +                                outtype = "html";
       +                        } else if (!strcasecmp(nodes[3].name, "content_text")) {
       +                                outtag = "content";
       +                                outtype = "text";
       +                        } else if (!strcasecmp(nodes[3].name, "date_published")) {
       +                                outtag = "published";
       +                        } else if (!strcasecmp(nodes[3].name, "date_modified")) {
       +                                outtag = "updated";
       +                        } else if (!strcasecmp(nodes[3].name, "id")) {
       +                                outtag = "id";
       +                        } else if (!strcasecmp(nodes[3].name, "summary")) {
       +                                outtag = "summary";
       +                        } else if (!strcasecmp(nodes[3].name, "title")) {
       +                                outtag = "title";
       +                        } else if (!strcasecmp(nodes[3].name, "url")) {
       +                                outtag = "link";
       +                                outhref = value;
       +                                value = NULL;
       +                        }
       +
       +                        if (outtag) {
       +                                fputs("\t<", stdout);
       +                                fputs(outtag, stdout);
       +                                if (outhref) {
       +                                        fputs(" href=\"", stdout);
       +                                        xmlencode(outhref, stdout);
       +                                        fputs("\"", stdout);
       +                                }
       +                                if (outtype) {
       +                                        fputs(" type=\"", stdout);
       +                                        xmlencode(outtype, stdout);
       +                                        fputs("\"", stdout);
       +                                }
       +                                fputs(">", stdout);
       +                                if (value)
       +                                        xmlencode(value, stdout);
       +                                fputs("</", stdout);
       +                                fputs(outtag, stdout);
       +                                fputs(">\n", stdout);
       +                        }
       +                }
       +        }
       +
       +        /* 1.0 author name */
       +        if (depth == 5) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    nodes[1].type == JSON_TYPE_ARRAY &&
       +                    nodes[2].type == JSON_TYPE_OBJECT &&
       +                    nodes[3].type == JSON_TYPE_OBJECT &&
       +                    nodes[4].type == JSON_TYPE_STRING &&
       +                    !strcasecmp(nodes[1].name, "items") &&
       +                    !strcasecmp(nodes[3].name, "author") &&
       +                    !strcasecmp(nodes[4].name, "name")) {
       +                        fputs("\t<author><name>", stdout);
       +                        xmlencode(value, stdout);
       +                        fputs("</name></author>\n", stdout);
       +                }
       +        }
       +
       +        /* 1.1 author name */
       +        if (depth == 6) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    nodes[1].type == JSON_TYPE_ARRAY &&
       +                    nodes[2].type == JSON_TYPE_OBJECT &&
       +                    nodes[3].type == JSON_TYPE_ARRAY &&
       +                    nodes[4].type == JSON_TYPE_OBJECT &&
       +                    nodes[5].type == JSON_TYPE_STRING &&
       +                    !strcasecmp(nodes[1].name, "items") &&
       +                    !strcasecmp(nodes[3].name, "authors") &&
       +                    !strcasecmp(nodes[5].name, "name")) {
       +                        fputs("\t<author><name>", stdout);
       +                        xmlencode(value, stdout);
       +                        fputs("</name></author>\n", stdout);
       +                }
       +        }
       +
       +        /* tags / categories */
       +        if (depth == 5) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    nodes[1].type == JSON_TYPE_ARRAY &&
       +                    nodes[2].type == JSON_TYPE_OBJECT &&
       +                    nodes[3].type == JSON_TYPE_ARRAY &&
       +                    nodes[4].type == JSON_TYPE_STRING &&
       +                    !strcasecmp(nodes[1].name, "items") &&
       +                    !strcasecmp(nodes[3].name, "tags")) {
       +                        fputs("\t<category term=\"", stdout);
       +                        xmlencode(value, stdout);
       +                        fputs("\" />\n", stdout);
       +                }
       +        }
       +
       +        /* enclosure */
       +        if (depth == 5) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    nodes[1].type == JSON_TYPE_ARRAY &&
       +                    nodes[2].type == JSON_TYPE_OBJECT &&
       +                    nodes[3].type == JSON_TYPE_ARRAY &&
       +                    nodes[4].type == JSON_TYPE_OBJECT &&
       +                    !strcasecmp(nodes[1].name, "items") &&
       +                    !strcasecmp(nodes[3].name, "attachments")) {
       +                        if (enclosureisopen)
       +                                fputs(" />\n", stdout);
       +                        fputs("\t<link rel=\"enclosure\"", stdout);
       +                        enclosureisopen = 1;
       +                }
       +        }
       +
       +        /* enclosure attributes */
       +        if (depth == 6) {
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    nodes[1].type == JSON_TYPE_ARRAY &&
       +                    nodes[2].type == JSON_TYPE_OBJECT &&
       +                    nodes[3].type == JSON_TYPE_ARRAY &&
       +                    nodes[4].type == JSON_TYPE_OBJECT &&
       +                    (nodes[5].type == JSON_TYPE_STRING || nodes[5].type == JSON_TYPE_NUMBER) &&
       +                    !strcasecmp(nodes[1].name, "items") &&
       +                    !strcasecmp(nodes[3].name, "attachments")) {
       +                        if (!strcasecmp(nodes[5].name, "url")) {
       +                                fputs(" href=\"", stdout);
       +                                xmlencode(value, stdout);
       +                                fputs("\"", stdout);
       +                        } else if (!strcasecmp(nodes[5].name, "mime_type")) {
       +                                fputs(" type=\"", stdout);
       +                                xmlencode(value, stdout);
       +                                fputs("\"", stdout);
       +                        } else if (!strcasecmp(nodes[5].name, "size_in_bytes")) {
       +                                fputs(" length=\"", stdout);
       +                                xmlencode(value, stdout);
       +                                fputs("\"", stdout);
       +                        }
       +                }
       +        }
       +
       +        if (ferror(stdout)) {
       +                fprintf(stderr, "write error: <stdout>\n");
       +                exit(2);
       +        }
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        if (pledge("stdio", NULL) == -1) {
       +                fprintf(stderr, "pledge stdio: %s\n", strerror(errno));
       +                return 1;
       +        }
       +
       +        fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
       +              "<feed xmlns=\"http://www.w3.org/2005/Atom\" xml:lang=\"en\">\n", stdout);
       +
       +        switch (parsejson(processnode)) {
       +        case JSON_ERROR_MEM:
       +                fputs("error: cannot allocate enough memory\n", stderr);
       +                return 2;
       +        case JSON_ERROR_INVALID:
       +                fputs("error: invalid JSON\n", stderr);
       +                return 1;
       +        }
       +
       +        if (enclosureisopen)
       +                fputs(" />\n", stdout);
       +        if (itemisopen)
       +                fputs("</entry>\n", stdout);
       +        fputs("</feed>\n", stdout);
       +
       +        if (ferror(stdin)) {
       +                fprintf(stderr, "read error: <stdin>\n");
       +                return 2;
       +        }
       +        if (fflush(stdout) || ferror(stdout)) {
       +                fprintf(stderr, "write error: <stdout>\n");
       +                return 2;
       +        }
       +
       +        return 0;
       +}
 (DIR) diff --git a/json.c b/json.c
       @@ -0,0 +1,319 @@
       +#include <errno.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#ifndef GETNEXT
       +#define GETNEXT getchar_unlocked
       +#endif
       +
       +#include "json.h"
       +
       +#define ISDIGIT(c) (((unsigned)c) - '0' < 10)
       +#define ISXDIGIT(c) ((((unsigned)c) - '0' < 10) || ((unsigned)c | 32) - 'a' < 6)
       +
       +static int
       +codepointtoutf8(long r, char *s)
       +{
       +        if (r == 0) {
       +                return 0; /* NUL byte */
       +        } else if (r <= 0x7F) {
       +                /* 1 byte: 0aaaaaaa */
       +                s[0] = r;
       +                return 1;
       +        } else if (r <= 0x07FF) {
       +                /* 2 bytes: 00000aaa aabbbbbb */
       +                s[0] = 0xC0 | ((r & 0x0007C0) >>  6); /* 110aaaaa */
       +                s[1] = 0x80 |  (r & 0x00003F);        /* 10bbbbbb */
       +                return 2;
       +        } else if (r <= 0xFFFF) {
       +                /* 3 bytes: aaaabbbb bbcccccc */
       +                s[0] = 0xE0 | ((r & 0x00F000) >> 12); /* 1110aaaa */
       +                s[1] = 0x80 | ((r & 0x000FC0) >>  6); /* 10bbbbbb */
       +                s[2] = 0x80 |  (r & 0x00003F);        /* 10cccccc */
       +                return 3;
       +        } else {
       +                /* 4 bytes: 000aaabb bbbbcccc ccdddddd */
       +                s[0] = 0xF0 | ((r & 0x1C0000) >> 18); /* 11110aaa */
       +                s[1] = 0x80 | ((r & 0x03F000) >> 12); /* 10bbbbbb */
       +                s[2] = 0x80 | ((r & 0x000FC0) >>  6); /* 10cccccc */
       +                s[3] = 0x80 |  (r & 0x00003F);        /* 10dddddd */
       +                return 4;
       +        }
       +}
       +
       +static int
       +hexdigit(int c)
       +{
       +        if (c >= '0' && c <= '9')
       +                return c - '0';
       +        else if (c >= 'a' && c <= 'f')
       +                return 10 + (c - 'a');
       +        else if (c >= 'A' && c <= 'F')
       +                return 10 + (c - 'A');
       +        return 0;
       +}
       +
       +static int
       +capacity(char **value, size_t *sz, size_t cur, size_t inc)
       +{
       +        size_t need, newsiz;
       +        char *newp;
       +
       +        /* check for addition overflow */
       +        if (cur > SIZE_MAX - inc) {
       +                errno = EOVERFLOW;
       +                return -1;
       +        }
       +        need = cur + inc;
       +
       +        if (need > *sz) {
       +                if (need > SIZE_MAX / 2) {
       +                        newsiz = SIZE_MAX;
       +                } else {
       +                        for (newsiz = *sz < 64 ? 64 : *sz; newsiz <= need; newsiz *= 2)
       +                                ;
       +                }
       +                if (!(newp = realloc(*value, newsiz)))
       +                        return -1; /* up to caller to free *value */
       +                *value = newp;
       +                *sz = newsiz;
       +        }
       +        return 0;
       +}
       +
       +#define EXPECT_VALUE         "{[\"-0123456789tfn"
       +#define EXPECT_STRING        "\""
       +#define EXPECT_END           "}],"
       +#define EXPECT_OBJECT_STRING EXPECT_STRING "}"
       +#define EXPECT_OBJECT_KEY    ":"
       +#define EXPECT_ARRAY_VALUE   EXPECT_VALUE "]"
       +
       +#define JSON_INVALID()       do { ret = JSON_ERROR_INVALID; goto end; } while (0);
       +
       +int
       +parsejson(void (*cb)(struct json_node *, size_t, const char *))
       +{
       +        struct json_node nodes[JSON_MAX_NODE_DEPTH] = { { 0 } };
       +        size_t depth = 0, p = 0, len, sz = 0;
       +        long cp, hi, lo;
       +        char pri[128], *str = NULL;
       +        int c, i, escape, iskey = 0, ret = JSON_ERROR_MEM;
       +        const char *expect = EXPECT_VALUE;
       +
       +        if (capacity(&(nodes[0].name), &(nodes[0].namesiz), 0, 1) == -1)
       +                goto end;
       +        nodes[0].name[0] = '\0';
       +
       +        while (1) {
       +                c = GETNEXT();
       +handlechr:
       +                if (c == EOF)
       +                        break;
       +
       +                /* skip JSON white-space, (NOTE: no \v, \f, \b etc) */
       +                if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
       +                        continue;
       +
       +                if (!c || !strchr(expect, c))
       +                        JSON_INVALID();
       +
       +                switch (c) {
       +                case ':':
       +                        iskey = 0;
       +                        expect = EXPECT_VALUE;
       +                        break;
       +                case '"':
       +                        nodes[depth].type = JSON_TYPE_STRING;
       +                        escape = 0;
       +                        len = 0;
       +                        while (1) {
       +                                c = GETNEXT();
       +chr:
       +                                /* EOF or control char: 0x7f is not defined as a control char in RFC8259 */
       +                                if (c < 0x20)
       +                                        JSON_INVALID();
       +
       +                                if (escape) {
       +escchr:
       +                                        escape = 0;
       +                                        switch (c) {
       +                                        case '"': /* FALLTHROUGH */
       +                                        case '\\':
       +                                        case '/': break;
       +                                        case 'b': c = '\b'; break;
       +                                        case 'f': c = '\f'; break;
       +                                        case 'n': c = '\n'; break;
       +                                        case 'r': c = '\r'; break;
       +                                        case 't': c = '\t'; break;
       +                                        case 'u': /* hex hex hex hex */
       +                                                if (capacity(&str, &sz, len, 4) == -1)
       +                                                        goto end;
       +                                                for (i = 12, cp = 0; i >= 0; i -= 4) {
       +                                                        if ((c = GETNEXT()) == EOF || !ISXDIGIT(c))
       +                                                                JSON_INVALID(); /* invalid code point */
       +                                                        cp |= (hexdigit(c) << i);
       +                                                }
       +                                                /* RFC8259 - 7. Strings - surrogates.
       +                                                 * 0xd800 - 0xdbff - high surrogates */
       +                                                if (cp >= 0xd800 && cp <= 0xdbff) {
       +                                                        if ((c = GETNEXT()) != '\\') {
       +                                                                len += codepointtoutf8(cp, &str[len]);
       +                                                                goto chr;
       +                                                        }
       +                                                        if ((c = GETNEXT()) != 'u') {
       +                                                                len += codepointtoutf8(cp, &str[len]);
       +                                                                goto escchr;
       +                                                        }
       +                                                        for (hi = cp, i = 12, lo = 0; i >= 0; i -= 4) {
       +                                                                if ((c = GETNEXT()) == EOF || !ISXDIGIT(c))
       +                                                                        JSON_INVALID(); /* invalid code point */
       +                                                                lo |= (hexdigit(c) << i);
       +                                                        }
       +                                                        /* 0xdc00 - 0xdfff - low surrogates */
       +                                                        if (lo >= 0xdc00 && lo <= 0xdfff) {
       +                                                                cp = (hi << 10) + lo - 56613888; /* - offset */
       +                                                        } else {
       +                                                                /* handle graceful: raw invalid output bytes */
       +                                                                len += codepointtoutf8(hi, &str[len]);
       +                                                                if (capacity(&str, &sz, len, 4) == -1)
       +                                                                        goto end;
       +                                                                len += codepointtoutf8(lo, &str[len]);
       +                                                                continue;
       +                                                        }
       +                                                }
       +                                                len += codepointtoutf8(cp, &str[len]);
       +                                                continue;
       +                                        default:
       +                                                JSON_INVALID(); /* invalid escape char */
       +                                        }
       +                                        if (capacity(&str, &sz, len, 1) == -1)
       +                                                goto end;
       +                                        str[len++] = c;
       +                                } else if (c == '\\') {
       +                                        escape = 1;
       +                                } else if (c == '"') {
       +                                        if (capacity(&str, &sz, len, 1) == -1)
       +                                                goto end;
       +                                        str[len++] = '\0';
       +
       +                                        if (iskey) {
       +                                                /* copy string as key, including NUL byte */
       +                                                if (capacity(&(nodes[depth].name), &(nodes[depth].namesiz), len, 1) == -1)
       +                                                        goto end;
       +                                                memcpy(nodes[depth].name, str, len);
       +                                        } else {
       +                                                cb(nodes, depth + 1, str);
       +                                        }
       +                                        break;
       +                                } else {
       +                                        if (capacity(&str, &sz, len, 1) == -1)
       +                                                goto end;
       +                                        str[len++] = c;
       +                                }
       +                        }
       +                        if (iskey)
       +                                expect = EXPECT_OBJECT_KEY;
       +                        else
       +                                expect = EXPECT_END;
       +                        break;
       +                case '[':
       +                case '{':
       +                        if (depth + 1 >= JSON_MAX_NODE_DEPTH)
       +                                JSON_INVALID(); /* too deep */
       +
       +                        nodes[depth].index = 0;
       +                        if (c == '[') {
       +                                nodes[depth].type = JSON_TYPE_ARRAY;
       +                                expect = EXPECT_ARRAY_VALUE;
       +                        } else if (c == '{') {
       +                                iskey = 1;
       +                                nodes[depth].type = JSON_TYPE_OBJECT;
       +                                expect = EXPECT_OBJECT_STRING;
       +                        }
       +
       +                        cb(nodes, depth + 1, "");
       +
       +                        depth++;
       +                        nodes[depth].index = 0;
       +                        if (capacity(&(nodes[depth].name), &(nodes[depth].namesiz), 0, 1) == -1)
       +                                goto end;
       +                        nodes[depth].name[0] = '\0';
       +                        break;
       +                case ']':
       +                case '}':
       +                        if (!depth ||
       +                           (c == ']' && nodes[depth - 1].type != JSON_TYPE_ARRAY) ||
       +                           (c == '}' && nodes[depth - 1].type != JSON_TYPE_OBJECT))
       +                                JSON_INVALID(); /* unbalanced nodes */
       +
       +                        depth--;
       +                        nodes[depth].index++;
       +                        expect = EXPECT_END;
       +                        break;
       +                case ',':
       +                        if (!depth)
       +                                JSON_INVALID(); /* unbalanced nodes */
       +
       +                        nodes[depth - 1].index++;
       +                        if (nodes[depth - 1].type == JSON_TYPE_OBJECT) {
       +                                iskey = 1;
       +                                expect = EXPECT_STRING;
       +                        } else {
       +                                expect = EXPECT_VALUE;
       +                        }
       +                        break;
       +                case 't': /* true */
       +                        if (GETNEXT() != 'r' || GETNEXT() != 'u' || GETNEXT() != 'e')
       +                                JSON_INVALID();
       +                        nodes[depth].type = JSON_TYPE_BOOL;
       +                        cb(nodes, depth + 1, "true");
       +                        expect = EXPECT_END;
       +                        break;
       +                case 'f': /* false */
       +                        if (GETNEXT() != 'a' || GETNEXT() != 'l' || GETNEXT() != 's' ||
       +                            GETNEXT() != 'e')
       +                                JSON_INVALID();
       +                        nodes[depth].type = JSON_TYPE_BOOL;
       +                        cb(nodes, depth + 1, "false");
       +                        expect = EXPECT_END;
       +                        break;
       +                case 'n': /* null */
       +                        if (GETNEXT() != 'u' || GETNEXT() != 'l' || GETNEXT() != 'l')
       +                                JSON_INVALID();
       +                        nodes[depth].type = JSON_TYPE_NULL;
       +                        cb(nodes, depth + 1, "null");
       +                        expect = EXPECT_END;
       +                        break;
       +                default: /* number */
       +                        nodes[depth].type = JSON_TYPE_NUMBER;
       +                        p = 0;
       +                        pri[p++] = c;
       +                        expect = EXPECT_END;
       +                        while (1) {
       +                                c = GETNEXT();
       +                                if (c == EOF ||
       +                                    (!ISDIGIT(c) && c != 'e' && c != 'E' &&
       +                                     c != '+' && c != '-' && c != '.') ||
       +                                    p + 1 >= sizeof(pri)) {
       +                                        pri[p] = '\0';
       +                                        cb(nodes, depth + 1, pri);
       +                                        goto handlechr; /* do not read next char, handle this */
       +                                } else {
       +                                        pri[p++] = c;
       +                                }
       +                        }
       +                }
       +        }
       +        if (depth)
       +                JSON_INVALID(); /* unbalanced nodes */
       +
       +        ret = 0; /* success */
       +end:
       +        for (depth = 0; depth < sizeof(nodes) / sizeof(nodes[0]); depth++)
       +                free(nodes[depth].name);
       +        free(str);
       +
       +        return ret;
       +}
 (DIR) diff --git a/json.h b/json.h
       @@ -0,0 +1,30 @@
       +#ifndef _JSON_H_
       +#define _JSON_H_
       +
       +#include <stddef.h>
       +
       +enum JSONType {
       +        JSON_TYPE_ARRAY  = 'a',
       +        JSON_TYPE_OBJECT = 'o',
       +        JSON_TYPE_STRING = 's',
       +        JSON_TYPE_BOOL   = 'b',
       +        JSON_TYPE_NULL   = '?',
       +        JSON_TYPE_NUMBER = 'n'
       +};
       +
       +enum JSONError {
       +        JSON_ERROR_MEM     = -2,
       +        JSON_ERROR_INVALID = -1
       +};
       +
       +#define JSON_MAX_NODE_DEPTH 64
       +
       +struct json_node {
       +        enum JSONType type;
       +        char *name;
       +        size_t namesiz;
       +        size_t index; /* count/index for array or object type */
       +};
       +
       +int parsejson(void (*cb)(struct json_node *, size_t, const char *));
       +#endif