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 <hiltjo@codemadness.org>
Date:   Mon,  3 Apr 2023 18:20:19 +0200

initial repo

Diffstat:
ALICENSE | 15+++++++++++++++
AMakefile | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME | 46++++++++++++++++++++++++++++++++++++++++++++++
Ajf2atom.1 | 39+++++++++++++++++++++++++++++++++++++++
Ajf2atom.c | 267+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajson.c | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajson.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("&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; +} 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