joeylib2/tools/common/toolXml.c

581 lines
16 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// toolXml.c - a small XML reader for the JoeyLib host tools (toolXml.h).
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "toolBuf.h"
#include "toolFile.h"
#include "toolXml.h"
#define UTF8_BOM "\xEF\xBB\xBF"
#define UTF8_BOM_BYTES 3u
#define ASCII_END 0x80u
#define UTF8_2_MAX 0x7FFu
#define UTF8_3_MAX 0xFFFFu
#define UNICODE_MAX 0x10FFFFu
#define UTF8_CONT 0x80u
#define UTF8_CONT_MASK 0x3Fu
#define UTF8_LEAD_2 0xC0u
#define UTF8_LEAD_3 0xE0u
#define UTF8_LEAD_4 0xF0u
#define UTF8_SHIFT 6u
#define DECIMAL 10u
#define HEX 16u
#define HEX_LETTER 10u
// The longest entity or character reference looked for, its & to its ;.
#define ENTITY_MAX 16u
typedef struct ParserT {
const char *text;
size_t len;
size_t pos;
char *err;
bool failed;
} ParserT;
static void attrAdd(ToolXmlT *node, char *name, char *value);
static void childAdd(ToolXmlT *node, ToolXmlT *child);
static ToolXmlT *element(ParserT *p);
static bool entity(ParserT *p, ToolBufT *out);
static void error(ParserT *p, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static bool isNameChar(char c);
static bool isNameStart(char c);
static bool isSpace(char c);
static bool lookingAt(const ParserT *p, const char *s);
static bool misc(ParserT *p);
static char *name(ParserT *p);
static bool skipPast(ParserT *p, const char *end);
static void skipSpace(ParserT *p);
static char *takeString(ToolBufT *buf);
static void utf8Put(ToolBufT *out, uint32_t code);
static void attrAdd(ToolXmlT *node, char *attrName, char *value) {
ToolXmlAttrT *attrs;
attrs = (ToolXmlAttrT *)realloc(node->attrs, (node->attrCount + 1u) * sizeof(ToolXmlAttrT));
if (attrs == NULL) {
toolFail("out of memory");
}
node->attrs = attrs;
node->attrs[node->attrCount].name = attrName;
node->attrs[node->attrCount].value = value;
node->attrCount++;
}
static void childAdd(ToolXmlT *node, ToolXmlT *child) {
ToolXmlT **children;
children = (ToolXmlT **)realloc(node->children, (node->childCount + 1u) * sizeof(ToolXmlT *));
if (children == NULL) {
toolFail("out of memory");
}
node->children = children;
node->children[node->childCount] = child;
node->childCount++;
}
// An element from its '<' to the end of its end tag, or NULL on an error.
static ToolXmlT *element(ParserT *p) {
ToolXmlT *node;
ToolXmlT *child;
ToolBufT value;
ToolBufT text;
ToolBufT tail;
char *attrName;
char *endName;
char quote;
char c;
bool sawChild;
p->pos++;
node = (ToolXmlT *)calloc(1u, sizeof(ToolXmlT));
if (node == NULL) {
toolFail("out of memory");
}
node->name = name(p);
if (node->name == NULL) {
free(node);
return NULL;
}
// Attributes, to the end of the start tag.
for (;;) {
skipSpace(p);
if (lookingAt(p, "/>")) {
p->pos += 2u;
node->text = takeString(NULL);
return node;
}
if (lookingAt(p, ">")) {
p->pos++;
break;
}
attrName = name(p);
if (attrName == NULL) {
toolXmlFree(node);
return NULL;
}
if (toolXmlAttr(node, attrName) != NULL) {
error(p, "duplicate attribute '%s'", attrName);
free(attrName);
toolXmlFree(node);
return NULL;
}
skipSpace(p);
if (!lookingAt(p, "=")) {
error(p, "no '=' after attribute '%s'", attrName);
free(attrName);
toolXmlFree(node);
return NULL;
}
p->pos++;
skipSpace(p);
if (p->pos >= p->len || (p->text[p->pos] != '"' && p->text[p->pos] != '\'')) {
error(p, "attribute '%s' is not quoted", attrName);
free(attrName);
toolXmlFree(node);
return NULL;
}
quote = p->text[p->pos++];
memset(&value, 0, sizeof(value));
while (p->pos < p->len && p->text[p->pos] != quote) {
c = p->text[p->pos];
if (c == '<') {
error(p, "'<' in attribute '%s'", attrName);
} else if (c == '&') {
entity(p, &value);
continue;
} else if (c == '\r' && p->pos + 1u < p->len && p->text[p->pos + 1u] == '\n') {
p->pos++;
continue;
} else {
// Attribute-value normalisation: a literal tab or line end
// is a space.
toolBufByte(&value, (c == '\t' || c == '\n' || c == '\r') ? ' ' : (uint8_t)c);
}
p->pos++;
}
if (p->failed || p->pos >= p->len) {
if (!p->failed) {
error(p, "unclosed attribute '%s'", attrName);
}
toolBufFree(&value);
free(attrName);
toolXmlFree(node);
return NULL;
}
p->pos++;
attrAdd(node, attrName, takeString(&value));
}
// Content, to the end tag: the text before the first child is kept.
memset(&text, 0, sizeof(text));
memset(&tail, 0, sizeof(tail));
sawChild = false;
for (;;) {
if (p->pos >= p->len) {
error(p, "no end tag for <%s>", node->name);
break;
}
if (lookingAt(p, "</")) {
p->pos += 2u;
endName = name(p);
if (endName == NULL) {
break;
}
skipSpace(p);
if (strcmp(endName, node->name) != 0 || !lookingAt(p, ">")) {
error(p, "mismatched tag: </%s> closes <%s>", endName, node->name);
free(endName);
break;
}
free(endName);
p->pos++;
node->text = takeString(&text);
toolBufFree(&tail);
return node;
}
if (lookingAt(p, "<![CDATA[")) {
p->pos += strlen("<![CDATA[");
while (p->pos < p->len && !lookingAt(p, "]]>")) {
if (!sawChild) {
toolBufByte(&text, (uint8_t)p->text[p->pos]);
}
p->pos++;
}
if (!skipPast(p, "]]>")) {
break;
}
continue;
}
if (lookingAt(p, "<!--") || lookingAt(p, "<?")) {
if (!misc(p)) {
break;
}
continue;
}
if (lookingAt(p, "<")) {
child = element(p);
if (child == NULL) {
break;
}
childAdd(node, child);
sawChild = true;
continue;
}
c = p->text[p->pos];
if (c == '&') {
// After the first child the text is a tail, not kept, but its
// entities must still be good ones.
entity(p, sawChild ? &tail : &text);
tail.len = 0u;
if (p->failed) {
break;
}
continue;
}
if (!sawChild) {
if (c == '\r') {
toolBufByte(&text, '\n');
if (p->pos + 1u < p->len && p->text[p->pos + 1u] == '\n') {
p->pos++;
}
} else {
toolBufByte(&text, (uint8_t)c);
}
}
p->pos++;
}
toolBufFree(&text);
toolBufFree(&tail);
toolXmlFree(node);
return NULL;
}
// An entity or character reference at p->pos, decoded into out; false (and
// an error) when it is not one.
static bool entity(ParserT *p, ToolBufT *out) {
static const char *const names[] = { "lt", "gt", "amp", "quot", "apos" };
static const char values[] = { '<', '>', '&', '"', '\'' };
size_t end;
size_t i;
size_t n;
uint32_t code;
uint32_t base;
uint32_t digit;
char c;
for (end = p->pos + 1u; end < p->len && p->text[end] != ';' && end - p->pos < ENTITY_MAX; end++) {
}
if (end >= p->len || p->text[end] != ';') {
error(p, "bad entity");
return false;
}
n = end - p->pos - 1u;
if (n >= 2u && p->text[p->pos + 1u] == '#') {
i = p->pos + 2u;
base = DECIMAL;
if (p->text[i] == 'x') {
base = HEX;
i++;
}
code = 0u;
if (i == end) {
error(p, "bad character reference");
return false;
}
for (; i < end; i++) {
c = p->text[i];
if (c >= '0' && c <= '9') {
digit = (uint32_t)(c - '0');
} else if (base == HEX && c >= 'a' && c <= 'f') {
digit = (uint32_t)(c - 'a') + HEX_LETTER;
} else if (base == HEX && c >= 'A' && c <= 'F') {
digit = (uint32_t)(c - 'A') + HEX_LETTER;
} else {
error(p, "bad character reference");
return false;
}
code = code * base + digit;
if (code > UNICODE_MAX) {
error(p, "bad character reference");
return false;
}
}
if (code == 0u) {
error(p, "bad character reference");
return false;
}
utf8Put(out, code);
p->pos = end + 1u;
return true;
}
for (i = 0; i < sizeof(values); i++) {
if (strlen(names[i]) == n && strncmp(p->text + p->pos + 1u, names[i], n) == 0) {
toolBufByte(out, (uint8_t)values[i]);
p->pos = end + 1u;
return true;
}
}
error(p, "undefined entity");
return false;
}
// The error, with the line it is on, unless one is already recorded.
static void error(ParserT *p, const char *fmt, ...) {
va_list args;
size_t i;
size_t line;
int n;
if (p->failed) {
return;
}
p->failed = true;
line = 1u;
for (i = 0; i < p->pos && i < p->len; i++) {
if (p->text[i] == '\n') {
line++;
}
}
n = snprintf(p->err, TOOL_XML_ERROR_BYTES, "line %zu: ", line);
if (n < 0 || (size_t)n >= TOOL_XML_ERROR_BYTES) {
return;
}
va_start(args, fmt);
vsnprintf(p->err + n, TOOL_XML_ERROR_BYTES - (size_t)n, fmt, args);
va_end(args);
}
static bool isNameChar(char c) {
return isNameStart(c) || (c >= '0' && c <= '9') || c == '-' || c == '.';
}
// A name starts with a letter, '_', ':', or any non-ASCII character.
static bool isNameStart(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':' || (unsigned char)c >= ASCII_END;
}
static bool isSpace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
static bool lookingAt(const ParserT *p, const char *s) {
size_t n;
n = strlen(s);
return p->pos + n <= p->len && memcmp(p->text + p->pos, s, n) == 0;
}
// Whitespace, a comment, a processing instruction, or a DOCTYPE skipped:
// true when there was one.
static bool misc(ParserT *p) {
size_t start;
if (p->failed) {
return false;
}
start = p->pos;
skipSpace(p);
if (lookingAt(p, "<!--")) {
return skipPast(p, "-->");
}
if (lookingAt(p, "<?")) {
return skipPast(p, "?>");
}
if (lookingAt(p, "<!DOCTYPE")) {
return skipPast(p, ">");
}
return p->pos != start;
}
// A name at p->pos, malloc'd; NULL (and an error) when there is none.
static char *name(ParserT *p) {
size_t start;
char *out;
start = p->pos;
if (p->pos >= p->len || !isNameStart(p->text[p->pos])) {
error(p, "not well-formed (a name was expected)");
return NULL;
}
while (p->pos < p->len && isNameChar(p->text[p->pos])) {
p->pos++;
}
out = (char *)malloc(p->pos - start + 1u);
if (out == NULL) {
toolFail("out of memory");
}
memcpy(out, p->text + start, p->pos - start);
out[p->pos - start] = '\0';
return out;
}
// Past the next end (inclusive); false (and an error) when it never comes.
static bool skipPast(ParserT *p, const char *end) {
size_t n;
n = strlen(end);
while (p->pos + n <= p->len) {
if (memcmp(p->text + p->pos, end, n) == 0) {
p->pos += n;
return true;
}
p->pos++;
}
error(p, "unclosed token (no '%s')", end);
return false;
}
static void skipSpace(ParserT *p) {
while (p->pos < p->len && isSpace(p->text[p->pos])) {
p->pos++;
}
}
// A buffer's bytes as a malloc'd C string (an empty one for NULL or an
// empty buffer); the buffer is handed over.
static char *takeString(ToolBufT *buf) {
char *out;
if (buf == NULL || buf->len == 0u) {
if (buf != NULL) {
toolBufFree(buf);
}
out = (char *)calloc(1u, 1u);
if (out == NULL) {
toolFail("out of memory");
}
return out;
}
toolBufByte(buf, '\0');
out = (char *)buf->data;
buf->data = NULL;
buf->len = 0u;
buf->cap = 0u;
return out;
}
const char *toolXmlAttr(const ToolXmlT *node, const char *attrName) {
size_t i;
for (i = 0; i < node->attrCount; i++) {
if (strcmp(node->attrs[i].name, attrName) == 0) {
return node->attrs[i].value;
}
}
return NULL;
}
const ToolXmlT *toolXmlChild(const ToolXmlT *node, const char *childName) {
size_t i;
for (i = 0; i < node->childCount; i++) {
if (strcmp(node->children[i]->name, childName) == 0) {
return node->children[i];
}
}
return NULL;
}
void toolXmlFree(ToolXmlT *node) {
size_t i;
if (node == NULL) {
return;
}
for (i = 0; i < node->attrCount; i++) {
free(node->attrs[i].name);
free(node->attrs[i].value);
}
for (i = 0; i < node->childCount; i++) {
toolXmlFree(node->children[i]);
}
free(node->attrs);
free(node->children);
free(node->name);
free(node->text);
free(node);
}
ToolXmlT *toolXmlParse(const char *text, size_t len, char err[TOOL_XML_ERROR_BYTES]) {
ParserT p;
ToolXmlT *root;
p.text = text;
p.len = len;
p.pos = 0u;
p.err = err;
p.failed = false;
err[0] = '\0';
if (lookingAt(&p, UTF8_BOM)) {
p.pos += UTF8_BOM_BYTES;
}
while (misc(&p)) {
}
if (p.failed) {
return NULL;
}
if (p.pos >= p.len || p.text[p.pos] != '<') {
error(&p, "no element found");
return NULL;
}
root = element(&p);
if (root == NULL) {
return NULL;
}
while (misc(&p)) {
}
if (!p.failed && p.pos < p.len) {
error(&p, "junk after document element");
}
if (p.failed) {
toolXmlFree(root);
return NULL;
}
return root;
}
// A code point as UTF-8.
static void utf8Put(ToolBufT *out, uint32_t code) {
if (code < ASCII_END) {
toolBufByte(out, code);
} else if (code <= UTF8_2_MAX) {
toolBufByte(out, UTF8_LEAD_2 | (code >> UTF8_SHIFT));
toolBufByte(out, UTF8_CONT | (code & UTF8_CONT_MASK));
} else if (code <= UTF8_3_MAX) {
toolBufByte(out, UTF8_LEAD_3 | (code >> (2u * UTF8_SHIFT)));
toolBufByte(out, UTF8_CONT | ((code >> UTF8_SHIFT) & UTF8_CONT_MASK));
toolBufByte(out, UTF8_CONT | (code & UTF8_CONT_MASK));
} else {
toolBufByte(out, UTF8_LEAD_4 | (code >> (3u * UTF8_SHIFT)));
toolBufByte(out, UTF8_CONT | ((code >> (2u * UTF8_SHIFT)) & UTF8_CONT_MASK));
toolBufByte(out, UTF8_CONT | ((code >> UTF8_SHIFT) & UTF8_CONT_MASK));
toolBufByte(out, UTF8_CONT | (code & UTF8_CONT_MASK));
}
}