// 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 #include #include #include #include #include #include #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, "&", 5); break; case '<': status = xmlBufPutBytes(buf, "<", 4); break; case '>': status = xmlBufPutBytes(buf, ">", 4); break; case '"': status = inAttribute ? xmlBufPutBytes(buf, """, 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, " ", 4) : xmlBufPutChar(buf, '\t'); break; case '\n': status = inAttribute ? xmlBufPutBytes(buf, " ", 5) : xmlBufPutChar(buf, '\n'); break; case '\r': status = xmlBufPutBytes(buf, " ", 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, "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 "&"); 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; }