initial repo - jfconvert - JSON Feed (subset) to sfeed or Atom converter | |
git clone git://git.codemadness.org/jfconvert | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
commit f7cde52eef12a6e77c28199a678de8665836e9e6 | |
Author: Hiltjo Posthuma <[email protected]> | |
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 <[email protected]> | |
+ | |
+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 <[email protected]> | |
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 | sf… | |
+.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 [email protected] | |
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_pa… | |
+ fputs("<link rel=\"alternate\" type=\"… | |
+ xmlencode(value, stdout); | |
+ fputs("\" />\n", stdout); | |
+ } else if (!strcasecmp(nodes[1].name, "descrip… | |
+ 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 == JSO… | |
+ !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"… | |
+ | |
+ 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' < … | |
+ | |
+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; new… | |
+ ; | |
+ } | |
+ 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 … | |
+ | |
+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… | |
+ 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… | |
+ goto end; | |
+ for (i = 12, cp = 0; i >= 0; i… | |
+ if ((c = GETNEXT()) ==… | |
+ JSON_INVALID()… | |
+ cp |= (hexdigit(c) << … | |
+ } | |
+ /* RFC8259 - 7. Strings - surr… | |
+ * 0xd800 - 0xdbff - high surr… | |
+ if (cp >= 0xd800 && cp <= 0xdb… | |
+ if ((c = GETNEXT()) !=… | |
+ len += codepoi… | |
+ goto chr; | |
+ } | |
+ if ((c = GETNEXT()) !=… | |
+ len += codepoi… | |
+ goto escchr; | |
+ } | |
+ for (hi = cp, i = 12, … | |
+ if ((c = GETNE… | |
+ JSON_I… | |
+ lo |= (hexdigi… | |
+ } | |
+ /* 0xdc00 - 0xdfff - l… | |
+ if (lo >= 0xdc00 && lo… | |
+ cp = (hi << 10… | |
+ } else { | |
+ /* handle grac… | |
+ len += codepoi… | |
+ if (capacity(&… | |
+ goto e… | |
+ len += codepoi… | |
+ continue; | |
+ } | |
+ } | |
+ len += codepointtoutf8(cp, &st… | |
+ continue; | |
+ default: | |
+ JSON_INVALID(); /* invalid esc… | |
+ } | |
+ 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, includi… | |
+ if (capacity(&(nodes[depth].na… | |
+ goto end; | |
+ memcpy(nodes[depth].name, str,… | |
+ } 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].name… | |
+ goto end; | |
+ nodes[depth].name[0] = '\0'; | |
+ break; | |
+ case ']': | |
+ case '}': | |
+ if (!depth || | |
+ (c == ']' && nodes[depth - 1].type != JSON_TYPE_ARR… | |
+ (c == '}' && nodes[depth - 1].type != JSON_TYPE_OBJ… | |
+ 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() … | |
+ 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() … | |
+ 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() … | |
+ 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 ch… | |
+ } 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 |