618 lines
20 KiB
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, "&", 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, "</", 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 "&"); 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;
|
|
}
|