/* -*- mode: c; c-file-style: "openbsd" -*- */
/*
* Copyright (c) 2017 Vincent Bernat <bernat@luffy.cx>
*
* 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.
*/
#if HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#include "writer.h"
#include "../compat/compat.h"
#include "../log.h"
enum tag {
STRING,
BOOL,
ARRAY,
OBJECT
};
struct element {
struct element *parent; /* Parent (if any) */
TAILQ_ENTRY(element) next; /* Sibling (if any) */
char *key; /* Key if parent is an object */
enum tag tag; /* Kind of element */
union {
char *string; /* STRING */
int boolean; /* BOOL */
TAILQ_HEAD(, element) children; /* ARRAY or OBJECT */
};
};
struct json_writer_private {
FILE *fh;
int variant;
struct element *root;
struct element *current; /* should always be an object */
};
/* Create a new element. If a parent is provided, it will also be attached to
* the parent. */
static struct element*
json_element_new(struct element *parent, const char *key, enum tag tag)
{
struct element *child = malloc(sizeof(*child));
if (child == NULL) fatal(NULL, NULL);
child->parent = parent;
child->key = key?strdup(key):NULL;
child->tag = tag;
TAILQ_INIT(&child->children);
if (parent) TAILQ_INSERT_TAIL(&parent->children, child, next);
return child;
}
/* Free the element content (but not the element itself) */
static void
json_element_free(struct element *current)
{
struct element *el, *el_next;
switch (current->tag) {
case STRING:
free(current->string);
break;
case BOOL:
break;
case ARRAY:
case OBJECT:
for (el = TAILQ_FIRST(¤t->children);
el != NULL;
el = el_next) {
el_next = TAILQ_NEXT(el, next);
json_element_free(el);
TAILQ_REMOVE(¤t->children, el, next);
if (current->tag == OBJECT) free(el->key);
free(el);
}
break;
}
}
static void
json_free(struct json_writer_private *p)
{
json_element_free(p->root);
free(p->root);
}
static void
json_string_dump(FILE *fh, const char *s)
{
fprintf(fh, "\"");
while (*s != '\0') {
unsigned int c = *s;
size_t len;
switch (c) {
case '"': fprintf(fh, "\\\""); s++; break;
case '\\': fprintf(fh, "\\\\"); s++; break;
case '\b': fprintf(fh, "\\b"); s++; break;
case '\f': fprintf(fh, "\\f"); s++; break;
case '\n': fprintf(fh, "\\n"); s++; break;
case '\r': fprintf(fh, "\\r"); s++; break;
case '\t': fprintf(fh, "\\t"); s++; break;
default:
len = utf8_validate_cz(s);
if (len == 0) {
/* Not a valid UTF-8 char, use a
* replacement character */
fprintf(fh, "\\uFFFD");
s++;
} else if (c < 0x1f) {
/* 7-bit ASCII character */
fprintf(fh, "\\u%04X", c);
s++;
} else {
/* UTF-8, write as is */
while (len--) fprintf(fh, "%c", *s++);
}
break;
}
}
fprintf(fh, "\"");
}
/* Dump an element to the specified file handle. */
static void
json_element_dump(FILE *fh, struct element *current, int indent)
{
static const char pairs[2][2] = { "{}", "[]" };
struct element *el;
switch (current->tag) {
case STRING:
json_string_dump(fh, current->string);
break;
case BOOL:
fprintf(fh, current->boolean?"true":"false");
break;
case ARRAY:
case OBJECT:
fprintf(fh, "%c\n%*s", pairs[(current->tag == ARRAY)][0],
indent + 2, "");
TAILQ_FOREACH(el, ¤t->children, next) {
if (current->tag == OBJECT)
fprintf(fh, "\"%s\": ", el->key);
json_element_dump(fh, el, indent + 2);
if (TAILQ_NEXT(el, next))
fprintf(fh, ",\n%*s", indent + 2, "");
}
fprintf(fh, "\n%*c", indent + 1,
pairs[(current->tag == ARRAY)][1]);
break;
}
}
static void
json_dump(struct json_writer_private *p)
{
json_element_dump(p->fh, p->root, 0);
fprintf(p->fh, "\n");
}
static void
json_start(struct writer *w, const char *tag,
const char *descr)
{
struct json_writer_private *p = w->priv;
struct element *child;
struct element *new;
/* Look for the tag in the current object. */
TAILQ_FOREACH(child, &p->current->children, next) {
if (!strcmp(child->key, tag)) break;
}
if (!child)
child = json_element_new(p->current, tag, ARRAY);
/* Queue the new element. */
new = json_element_new(child, NULL, OBJECT);
p->current = new;
}
static void
json_attr(struct writer *w, const char *tag,
const char *descr, const char *value)
{
struct json_writer_private *p = w->priv;
struct element *new = json_element_new(p->current, tag, STRING);
if (value && (!strcmp(value, "yes") || !strcmp(value, "on"))) {
new->tag = BOOL;
new->boolean = 1;
} else if (value && (!strcmp(value, "no") || !strcmp(value, "off"))) {
new->tag = BOOL;
new->boolean = 0;
} else {
new->string = strdup(value?value:"");
}
}
static void
json_data(struct writer *w, const char *data)
{
struct json_writer_private *p = w->priv;
struct element *new = json_element_new(p->current, "value", STRING);
new->string = strdup(data?data:"");
}
/* When an array has only one member, just remove the array. When an object has
* `value` as the only key, remove the object. Moreover, for an object, move the
* `name` key outside (inside a new object). This is a recursive function. We
* think the depth will be limited. Also, the provided element can be
* destroyed. Don't use it after this function!
*
* During the cleaning process, we will generate array of 1-size objects that
* could be turned into an object. We don't do that since people may rely on
* this format. Another problem is the format is changing depending on the
* number of interfaces or the number of neighbors.
*/
static void
json_element_cleanup(struct element *el)
{
#ifndef ENABLE_JSON0
struct element *child, *child_next;
/* If array with one element, steal the content. Object with only one
* value whose key is "value", steal the content. */
if ((el->tag == ARRAY || el->tag == OBJECT) &&
(child = TAILQ_FIRST(&el->children)) &&
!TAILQ_NEXT(child, next) &&
(el->tag == ARRAY || !strcmp(child->key, "value"))) {
free(child->key);
child->key = el->key;
child->parent = el->parent;
TAILQ_INSERT_BEFORE(el, child, next);
TAILQ_REMOVE(&el->parent->children, el, next);
free(el);
json_element_cleanup(child);
return;
}
/* Other kind of arrays, recursively clean */
if (el->tag == ARRAY) {
for (child = TAILQ_FIRST(&el->children);
child;
child = child_next) {
child_next = TAILQ_NEXT(child, next);
json_element_cleanup(child);
}
return;
}
/* Other kind of objects, recursively clean, but if one key is "name",
* use it's value as a key for a new object stealing the existing
* one. */
if (el->tag == OBJECT) {
struct element *name_child = NULL;
for (child = TAILQ_FIRST(&el->children);
child;
child = child_next) {
child_next = TAILQ_NEXT(child, next);
json_element_cleanup(child);
}
/* Redo a check to find if we have a "name" key now */
for (child = TAILQ_FIRST(&el->children);
child;
child = child_next) {
child_next = TAILQ_NEXT(child, next);
if (!strcmp(child->key, "name") &&
child->tag == STRING) {
name_child = child;
}
}
if (name_child) {
struct element *new_el = json_element_new(NULL, NULL, OBJECT);
/* Replace el by new_el in parent object/array */
new_el->parent = el->parent;
TAILQ_INSERT_BEFORE(el, new_el, next);
TAILQ_REMOVE(&el->parent->children, el, next);
new_el->key = el->key;
/* new_el is parent of el */
el->parent = new_el;
el->key = name_child->string; /* stolen */
TAILQ_INSERT_TAIL(&new_el->children, el, next);
/* Remove "name" child */
TAILQ_REMOVE(&el->children, name_child, next);
free(name_child->key);
free(name_child);
}
return;
}
#endif
}
static void
json_cleanup(struct json_writer_private *p)
{
if (p->variant != 0)
json_element_cleanup(p->root);
}
static void
json_end(struct writer *w)
{
struct json_writer_private *p = w->priv;
while ((p->current = p->current->parent) != NULL && p->current->tag != OBJECT);
if (p->current == NULL) {
fatalx("lldpctl", "unbalanced tags");
return;
}
/* Display current object if last one */
if (p->current == p->root) {
json_cleanup(p);
json_dump(p);
json_free(p);
fprintf(p->fh,"\n");
fflush(p->fh);
p->root = p->current = json_element_new(NULL, NULL, OBJECT);
}
}
static void
json_finish(struct writer *w)
{
struct json_writer_private *p = w->priv;
if (p->current != p->root)
log_warnx("lldpctl", "unbalanced tags");
json_free(p);
free(p);
free(w);
}
struct writer*
json_init(FILE *fh, int variant)
{
struct writer *result;
struct json_writer_private *priv;
priv = malloc(sizeof(*priv));
if (priv == NULL) fatal(NULL, NULL);
priv->fh = fh;
priv->root = priv->current = json_element_new(NULL, NULL, OBJECT);
priv->variant = variant;
result = malloc(sizeof(*result));
if (result == NULL) fatal(NULL, NULL);
result->priv = priv;
result->start = json_start;
result->attr = json_attr;
result->data = json_data;
result->end = json_end;
result->finish = json_finish;
return result;
}