581 lines
16 KiB
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 normalization: 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));
|
|
}
|
|
}
|