calog/libs/calogXml.c

618 lines
20 KiB
C

// calogXml.c -- calog XML library (see calogXml.h). Parsing rides vendored libxml2 (SAX) and builds
// an element tree in calog's aggregate model; serializing walks that tree back to XML text. The two
// natives are pure functions of their arguments; libxml2 keeps process-global state, initialized in
// calogXmlRegister and released in calogXmlShutdown.
#define _POSIX_C_SOURCE 200809L
#include "calogXml.h"
#include "calogInternal.h"
#include <libxml/parser.h>
#include <libxml/xmlerror.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define XML_BUF_INITIAL 64
// A growable output byte buffer, used both for serialization and for coalescing a run of
// character data before it becomes one text child.
typedef struct XmlBufT {
char *bytes;
size_t length;
size_t cap;
} XmlBufT;
// One open element while parsing: the child list being filled, the attribute map, and any
// character data seen so far that has not yet been committed as a text child.
typedef struct XmlFrameT {
CalogAggT *children;
CalogAggT *attr;
XmlBufT text;
} XmlFrameT;
// The whole parse in flight. frames[0, depth) are the open elements (outermost first). status is
// sticky: once set, every handler becomes a no-op and the failure is reported. root holds the tree.
typedef struct XmlParseT {
XmlFrameT *frames;
int32_t depth;
int32_t status;
CalogValueT root;
bool haveRoot;
char error[192];
bool haveError;
} XmlParseT;
static int32_t xmlBufEnsure(XmlBufT *buf, size_t extra);
static void xmlBufFree(XmlBufT *buf);
static int32_t xmlBufPutBytes(XmlBufT *buf, const char *bytes, size_t length);
static int32_t xmlBufPutChar(XmlBufT *buf, char c);
static int32_t xmlBufPutEscaped(XmlBufT *buf, const char *bytes, int64_t length, bool inAttribute);
static void xmlCharData(void *userData, const xmlChar *text, int length);
static int32_t xmlEncodeElement(XmlBufT *buf, CalogAggT *element, int32_t depth);
static void xmlEndElement(void *userData, const xmlChar *name);
static void xmlErrorSink(void *userData, const xmlError *error);
static void xmlFrameFree(XmlFrameT *frame);
static CalogValueT *xmlMapGet(CalogAggT *map, const char *name);
static int32_t xmlParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t xmlPushText(CalogAggT *children, XmlBufT *text);
static int32_t xmlSetAgg(CalogAggT *map, const char *key, CalogAggT *child);
static int32_t xmlSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length);
static void xmlStartElement(void *userData, const xmlChar *name, const xmlChar **atts);
static void xmlStop(XmlParseT *state, int32_t status);
static int32_t xmlStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogXmlRegister(CalogT *calog) {
// libxml2 self-initializes, thread-safely (mutex-guarded), when a parser context is created, so
// there is no explicit init to do and nothing process-global to tear down: its per-thread state
// is released when each context thread exits.
calogRegisterInline(calog, "xmlParse", xmlParseNative, NULL);
calogRegisterInline(calog, "xmlStringify", xmlStringifyNative, NULL);
return calogOkE;
}
static int32_t xmlBufEnsure(XmlBufT *buf, size_t extra) {
size_t need;
size_t cap;
char *grown;
need = buf->length + extra;
if (need <= buf->cap) {
return calogOkE;
}
cap = (buf->cap == 0) ? XML_BUF_INITIAL : buf->cap;
while (cap < need) {
cap *= CALOG_GROWTH_FACTOR;
}
grown = (char *)realloc(buf->bytes, cap);
if (grown == NULL) {
return calogErrOomE;
}
buf->bytes = grown;
buf->cap = cap;
return calogOkE;
}
static void xmlBufFree(XmlBufT *buf) {
free(buf->bytes);
buf->bytes = NULL;
buf->length = 0;
buf->cap = 0;
}
static int32_t xmlBufPutBytes(XmlBufT *buf, const char *bytes, size_t length) {
int32_t status;
if (length == 0) {
return calogOkE;
}
status = xmlBufEnsure(buf, length);
if (status != calogOkE) {
return status;
}
memcpy(buf->bytes + buf->length, bytes, length);
buf->length += length;
return calogOkE;
}
static int32_t xmlBufPutChar(XmlBufT *buf, char c) {
return xmlBufPutBytes(buf, &c, 1);
}
static int32_t xmlBufPutEscaped(XmlBufT *buf, const char *bytes, int64_t length, bool inAttribute) {
int64_t index;
int32_t status;
for (index = 0; index < length; index++) {
unsigned char c;
c = (unsigned char)bytes[index];
switch (c) {
case '&':
status = xmlBufPutBytes(buf, "&amp;", 5);
break;
case '<':
status = xmlBufPutBytes(buf, "&lt;", 4);
break;
case '>':
status = xmlBufPutBytes(buf, "&gt;", 4);
break;
case '"':
status = inAttribute ? xmlBufPutBytes(buf, "&quot;", 6) : xmlBufPutChar(buf, '"');
break;
// XML rewrites literal whitespace on re-parse: attribute-value normalization turns a raw
// TAB/newline into a space, and end-of-line handling turns a raw CR into a newline. Emit
// them as numeric references so the exact bytes survive a parse -> stringify -> parse.
case '\t':
status = inAttribute ? xmlBufPutBytes(buf, "&#9;", 4) : xmlBufPutChar(buf, '\t');
break;
case '\n':
status = inAttribute ? xmlBufPutBytes(buf, "&#10;", 5) : xmlBufPutChar(buf, '\n');
break;
case '\r':
status = xmlBufPutBytes(buf, "&#13;", 5);
break;
default:
status = xmlBufPutChar(buf, (char)c);
break;
}
if (status != calogOkE) {
return status;
}
}
return calogOkE;
}
static void xmlCharData(void *userData, const xmlChar *text, int length) {
XmlParseT *state;
int32_t status;
state = (XmlParseT *)userData;
if (state->status != calogOkE) {
return;
}
if (state->depth == 0 || length <= 0) {
return; // character data outside the root (libxml2 only delivers whitespace here)
}
status = xmlBufPutBytes(&state->frames[state->depth - 1].text, (const char *)text, (size_t)length);
if (status != calogOkE) {
xmlStop(state, status);
}
}
static int32_t xmlEncodeElement(XmlBufT *buf, CalogAggT *element, int32_t depth) {
CalogValueT *tag;
CalogValueT *attr;
CalogValueT *children;
int32_t status;
int64_t index;
if (depth >= CALOG_MAX_DEPTH) {
return calogErrDepthE;
}
tag = xmlMapGet(element, "tag");
if (tag == NULL || tag->type != calogStringE) {
return calogErrTypeE;
}
status = xmlBufPutChar(buf, '<');
if (status != calogOkE) {
return status;
}
status = xmlBufPutBytes(buf, tag->as.s.bytes, (size_t)tag->as.s.length);
if (status != calogOkE) {
return status;
}
attr = xmlMapGet(element, "attr");
if (attr != NULL && attr->type == calogAggE) {
CalogAggT *map;
map = attr->as.agg;
for (index = 0; index < map->pairCount; index++) {
CalogValueT *key;
CalogValueT *value;
key = &map->pairs[index].key;
value = &map->pairs[index].value;
if (key->type != calogStringE || value->type != calogStringE) {
return calogErrTypeE;
}
status = xmlBufPutChar(buf, ' ');
if (status != calogOkE) {
return status;
}
status = xmlBufPutBytes(buf, key->as.s.bytes, (size_t)key->as.s.length);
if (status != calogOkE) {
return status;
}
status = xmlBufPutBytes(buf, "=\"", 2);
if (status != calogOkE) {
return status;
}
status = xmlBufPutEscaped(buf, value->as.s.bytes, value->as.s.length, true);
if (status != calogOkE) {
return status;
}
status = xmlBufPutChar(buf, '"');
if (status != calogOkE) {
return status;
}
}
}
children = xmlMapGet(element, "children");
if (children == NULL || children->type != calogAggE || children->as.agg->arrayCount == 0) {
return xmlBufPutBytes(buf, "/>", 2); // empty element -> self-closing tag
}
status = xmlBufPutChar(buf, '>');
if (status != calogOkE) {
return status;
}
for (index = 0; index < children->as.agg->arrayCount; index++) {
CalogValueT *child;
child = &children->as.agg->array[index];
if (child->type == calogStringE) {
status = xmlBufPutEscaped(buf, child->as.s.bytes, child->as.s.length, false);
} else if (child->type == calogAggE) {
status = xmlEncodeElement(buf, child->as.agg, depth + 1);
} else {
return calogErrTypeE;
}
if (status != calogOkE) {
return status;
}
}
status = xmlBufPutBytes(buf, "</", 2);
if (status != calogOkE) {
return status;
}
status = xmlBufPutBytes(buf, tag->as.s.bytes, (size_t)tag->as.s.length);
if (status != calogOkE) {
return status;
}
return xmlBufPutChar(buf, '>');
}
static void xmlEndElement(void *userData, const xmlChar *name) {
XmlParseT *state;
XmlFrameT *top;
CalogAggT *element;
CalogValueT elementValue;
int32_t status;
state = (XmlParseT *)userData;
if (state->status != calogOkE) {
return;
}
top = &state->frames[state->depth - 1];
status = xmlPushText(top->children, &top->text);
if (status != calogOkE) {
xmlStop(state, status);
return;
}
xmlBufFree(&top->text); // this element is closing: no more character data joins it
status = calogAggCreate(&element, calogMapE);
if (status != calogOkE) {
xmlStop(state, status);
return;
}
status = xmlSetStr(element, "tag", (const char *)name, (int64_t)strlen((const char *)name));
if (status != calogOkE) {
calogAggFree(element);
xmlStop(state, status);
return;
}
if (top->attr != NULL && top->attr->pairCount > 0) {
status = xmlSetAgg(element, "attr", top->attr);
top->attr = NULL; // consumed by element (or freed by xmlSetAgg on failure)
if (status != calogOkE) {
calogAggFree(element);
xmlStop(state, status);
return;
}
} else {
if (top->attr != NULL) {
calogAggFree(top->attr);
}
top->attr = NULL;
}
status = xmlSetAgg(element, "children", top->children);
top->children = NULL; // consumed by element (or freed by xmlSetAgg on failure)
if (status != calogOkE) {
calogAggFree(element);
xmlStop(state, status);
return;
}
state->depth--; // the frame's resources are now owned by element
calogValueAgg(&elementValue, element);
if (state->depth > 0) {
status = calogAggPush(state->frames[state->depth - 1].children, &elementValue);
if (status != calogOkE) {
calogValueFree(&elementValue);
xmlStop(state, status);
return;
}
} else {
calogValueMove(&state->root, &elementValue);
state->haveRoot = true;
}
}
static void xmlErrorSink(void *userData, const xmlError *error) {
XmlParseT *state;
state = (XmlParseT *)userData;
if (error == NULL || error->level < XML_ERR_ERROR) {
return; // ignore warnings; only real errors fail the parse
}
if (state->status == calogOkE) {
state->status = calogErrArgE;
}
if (!state->haveError && error->message != NULL) {
size_t length;
snprintf(state->error, sizeof(state->error), "xmlParse: %s", error->message);
length = strlen(state->error);
while (length > 0 && (state->error[length - 1] == '\n' || state->error[length - 1] == '\r')) {
length--;
state->error[length] = '\0';
}
state->haveError = true;
}
}
static void xmlFrameFree(XmlFrameT *frame) {
if (frame->children != NULL) {
calogAggFree(frame->children);
frame->children = NULL;
}
if (frame->attr != NULL) {
calogAggFree(frame->attr);
frame->attr = NULL;
}
xmlBufFree(&frame->text);
}
static CalogValueT *xmlMapGet(CalogAggT *map, const char *name) {
CalogValueT key;
CalogValueT *found;
if (calogValueString(&key, name, (int64_t)strlen(name)) != calogOkE) {
return NULL;
}
found = calogAggGet(map, &key);
calogValueFree(&key);
return found;
}
static int32_t xmlParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
XmlParseT state;
xmlSAXHandler sax;
xmlParserCtxtPtr ctxt;
int32_t index;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "xmlParse expects (text)");
}
if (args[0].as.s.length > INT_MAX) {
return calogFail(result, calogErrArgE, "xmlParse: input too large");
}
memset(&state, 0, sizeof(state));
state.frames = (XmlFrameT *)calloc((size_t)CALOG_MAX_DEPTH, sizeof(XmlFrameT));
if (state.frames == NULL) {
return calogFail(result, calogErrOomE, "xmlParse: out of memory");
}
memset(&sax, 0, sizeof(sax));
sax.initialized = 0; // SAX1 element callbacks (qnames, no namespace splitting)
sax.startElement = xmlStartElement;
sax.endElement = xmlEndElement;
sax.characters = xmlCharData;
sax.cdataBlock = xmlCharData; // treat CDATA content as ordinary text
ctxt = xmlCreatePushParserCtxt(&sax, &state, NULL, 0, NULL);
if (ctxt == NULL) {
free(state.frames);
return calogFail(result, calogErrOomE, "xmlParse: out of memory");
}
xmlCtxtSetErrorHandler(ctxt, xmlErrorSink, &state);
// NOENT substitutes predefined and internal entities so attribute values decode (the SAX
// interface otherwise reports "&" in an attribute as "&#38;"); NO_XXE and NONET then block all
// external content and network access, so no entity reference can reach a local file or URL --
// XXE-safe. NOCDATA delivers CDATA content through the characters callback.
xmlCtxtUseOptions(ctxt, XML_PARSE_NONET | XML_PARSE_NOCDATA | XML_PARSE_NOENT | XML_PARSE_NO_XXE);
xmlParseChunk(ctxt, args[0].as.s.bytes, (int)args[0].as.s.length, 1);
xmlFreeParserCtxt(ctxt);
if (state.status == calogOkE && !state.haveRoot) {
state.status = calogErrArgE;
state.haveError = false; // fall through to the "no root" message below
}
if (state.status != calogOkE) {
const char *message;
if (state.haveError) {
message = state.error;
} else if (state.status == calogErrDepthE) {
message = "xmlParse: nesting exceeds the maximum depth";
} else if (state.status == calogErrOomE) {
message = "xmlParse: out of memory";
} else {
message = "xmlParse: no root element";
}
for (index = 0; index < state.depth; index++) {
xmlFrameFree(&state.frames[index]);
}
if (state.haveRoot) {
calogValueFree(&state.root);
}
free(state.frames);
return calogFail(result, state.status, message);
}
free(state.frames);
calogValueMove(result, &state.root);
return calogOkE;
}
static int32_t xmlPushText(CalogAggT *children, XmlBufT *text) {
CalogValueT value;
int32_t status;
if (text->length == 0) {
return calogOkE;
}
status = calogValueString(&value, text->bytes, (int64_t)text->length);
if (status != calogOkE) {
return status;
}
status = calogAggPush(children, &value);
if (status != calogOkE) {
calogValueFree(&value);
return status;
}
text->length = 0; // committed; keep the buffer for any following run of text
return calogOkE;
}
static int32_t xmlSetAgg(CalogAggT *map, const char *key, CalogAggT *child) {
CalogValueT keyValue;
CalogValueT childValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
calogAggFree(child);
return status;
}
calogValueAgg(&childValue, child);
status = calogAggSet(map, &keyValue, &childValue);
if (status != calogOkE) {
calogValueFree(&keyValue);
calogValueFree(&childValue); // frees child too
}
return status;
}
static int32_t xmlSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length) {
CalogValueT keyValue;
CalogValueT stringValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
return status;
}
status = calogValueString(&stringValue, bytes, length);
if (status != calogOkE) {
calogValueFree(&keyValue);
return status;
}
status = calogAggSet(map, &keyValue, &stringValue);
if (status != calogOkE) {
calogValueFree(&keyValue);
calogValueFree(&stringValue);
}
return status;
}
static void xmlStartElement(void *userData, const xmlChar *name, const xmlChar **atts) {
XmlParseT *state;
XmlFrameT *frame;
CalogAggT *children;
CalogAggT *attr;
int32_t status;
int32_t index;
(void)name;
state = (XmlParseT *)userData;
if (state->status != calogOkE) {
return;
}
if (state->depth > 0) {
// Character data seen before this child becomes a text node preceding it.
status = xmlPushText(state->frames[state->depth - 1].children, &state->frames[state->depth - 1].text);
if (status != calogOkE) {
xmlStop(state, status);
return;
}
}
if (state->depth >= CALOG_MAX_DEPTH) {
xmlStop(state, calogErrDepthE);
return;
}
status = calogAggCreate(&children, calogListE);
if (status != calogOkE) {
xmlStop(state, status);
return;
}
status = calogAggCreate(&attr, calogMapE);
if (status != calogOkE) {
calogAggFree(children);
xmlStop(state, status);
return;
}
if (atts != NULL) {
for (index = 0; atts[index] != NULL; index += 2) {
status = xmlSetStr(attr, (const char *)atts[index], (const char *)atts[index + 1],
(int64_t)strlen((const char *)atts[index + 1]));
if (status != calogOkE) {
calogAggFree(children);
calogAggFree(attr);
xmlStop(state, status);
return;
}
}
}
frame = &state->frames[state->depth];
frame->children = children;
frame->attr = attr;
frame->text.bytes = NULL;
frame->text.length = 0;
frame->text.cap = 0;
state->depth++;
}
static void xmlStop(XmlParseT *state, int32_t status) {
if (state->status == calogOkE) {
state->status = status;
}
// Handlers guard on state->status, so subsequent SAX callbacks become no-ops from here.
}
static int32_t xmlStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
XmlBufT buf;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogAggE) {
return calogFail(result, calogErrArgE, "xmlStringify expects (element map)");
}
buf.bytes = NULL;
buf.length = 0;
buf.cap = 0;
status = xmlEncodeElement(&buf, args[0].as.agg, 0);
if (status != calogOkE) {
xmlBufFree(&buf);
return calogFail(result, status, "xmlStringify: value is not a valid XML element");
}
status = calogValueString(result, (buf.bytes != NULL) ? buf.bytes : "", (int64_t)buf.length);
xmlBufFree(&buf);
return status;
}