Search guides and the APICtrl K
  1. API reference
  2. Data, text and crypto

xml.h

XML: a tree of elements, read from text and written back.

#include <sonore/xml.h>
  • 2types
On this page

Design notes

Why there is an XML parser here after all

For a long time the answer was no: plugin state is a StateBag, and json.h covers the one place a human-readable format was wanted. What changed is what a plugin has to READ. A VST3 preset's metadata chunk is XML. An .aupreset is an XML property list. A DAWproject file is a zip of XML. A plugin ported from the reference brings years of users' presets saved as that framework's XML state, and "your old presets do not open" is not a migration story anybody accepts. None of those formats is going to become JSON because this SDK would prefer it.

The API is the reference's shape -- XmlElement with typed attribute getters, child lookup by name and by attribute, text elements; XmlDocument with getLastParseError -- so code moving across reads the same.

What it is safe against

The input is a file somebody downloaded, so it is treated as hostile:

  • No external anything. A DOCTYPE is read past and its contents are ignored; nothing is ever fetched or opened, so there is no XXE.
  • No entity expansion beyond the five predefined entities and numeric character references. That is what makes "billion laughs" impossible rather than merely limited: an entity declared in an internal subset is not expanded, and referencing it is an error that names it.
  • Limits on input size, nesting depth and node count (attributes count as nodes), all checked while parsing -- a refusal, not a stack overflow or an out-of-memory.
  • Nothing recursive. Parsing, writing, copying, comparing and destroying all walk the tree with an explicit stack, so even a tree built by hand a million levels deep cannot exhaust the thread's stack -- which on a host's background thread may be 512 KB.
  • Duplicate attributes are found by sorting, not by comparing every pair: an element with a hundred thousand attributes is a refusal in milliseconds rather than a hang.

What it does not do, and the leniencies, named

UTF-8 in, UTF-8 out. A declared encoding of ASCII or ISO-8859-1 is accepted (Latin-1 is transcoded first, byte for code point); UTF-16 and anything else are refused by name rather than misread. No validation against a DTD or schema, no namespace processing beyond prefix helpers.

Strict where it matters -- invalid UTF-8, raw control characters, unknown entities, mismatched tags, '<' inside an attribute value are all errors -- and lenient in three places where real files break the letter of the spec and nothing is at stake: "--" inside a comment, "]]>" in text, and numeric references to control characters (&#1;), which the writer itself emits so a string containing one survives the round trip. Names follow XML's ASCII rules and accept any non-ASCII character.

Whitespace-only text between elements is dropped by default (the reference's default too); comments and processing instructions are read and dropped unless comments are asked for.

There is no converter to JsonValue: attributes beside children, children whose order matters, and text mixed between elements have no one JSON shape, and a converter would pick one silently. A plugin that keeps XML in its state stores toString(TextFormat().singleLine()) as a StateBag string and parses it back on load -- the reference's copyXmlToBinary, in one line.

Types 2

One node of an XML tree: an element with a tag name, attributes and children -- or, with an empty tag name, a text node or a comment.

Text is held in child nodes, as the reference does it, so mixed content (<p>one <b>two</b> three</p>) keeps its order. Children are owned; a pointer returned by any of the child functions stays valid until that child is removed or its parent destroyed.

Parses XML text into an XmlElement tree.

XmlDocument doc(text);
std::unique_ptr<XmlElement> root = doc.getDocumentElement();
if (!root) report(doc.getLastParseError());   // "line 3, column 14: ..."

or in one call, XmlDocument::parse(text, &error).

↑↓ to move↵ to openEvery public name in the headers, and every guide