// 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 #include #include #include #include #include #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, "pos += 2u; endName = name(p); if (endName == NULL) { break; } skipSpace(p); if (strcmp(endName, node->name) != 0 || !lookingAt(p, ">")) { error(p, "mismatched tag: closes <%s>", endName, node->name); free(endName); break; } free(endName); p->pos++; node->text = takeString(&text); toolBufFree(&tail); return node; } if (lookingAt(p, "pos += strlen("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, ""); } if (lookingAt(p, ""); } if (lookingAt(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)); } }