commit f7cde52eef12a6e77c28199a678de8665836e9e6
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(-)
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.
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
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>
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
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("<", fp); break;
+ case '>': fputs(">", fp); break;
+ case '\'': fputs("'", fp); break;
+ case '&': fputs("&", fp); break;
+ case '"': fputs(""", 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;
+}
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;
+}
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