
xmloxide v0.5.0
Eine reine Rust-Neuumsetzung von libxml2
xmloxide
Eine reine Rust-Neuimplementierung von libxml2 — dem De-facto-Standard für XML/HTML-Parsing-Bibliotheken in der Open-Source-Welt.
libxml2 wurde im Dezember 2025 offiziell für ungepflegt erklärt und weist bekannte Sicherheitslücken auf. xmloxide zielt darauf ab, ein speichersicherer, leistungsstarker Ersatz zu sein, der die gleichen Konformitätstestsuiten besteht.
Funktionen
- Speichersicher — Arena-basierter Baum ohne
unsafein der öffentlichen API - Konform — 100% Bestehensquote der W3C-XML-Konformitätstestsuite (1727/1727 anwendbare Tests)
- Fehlerbehandlung — fehlerhafte XML-Daten parsen und trotzdem einen nutzbaren Baum erzeugen, genau wie libxml2
- Mehrere Parsing-APIs — DOM-Baum, SAX2-Streaming, XmlReader-Pull, Push/inkrementell
- HTML-Parser — fehlertolerantes HTML-4.01-Parsen mit automatischem Schließen und void-Elementen
- WHATWG-HTML5-Parser — vollständiger HTML Living Standard-Tokenizer und Baumkonstruktor (8810/8810 html5lib-Tests bestanden)
- HTML5-Streaming — SAX-ähnliche Callback-API für HTML5 (
html5::sax), die den Tokenizer umschließt, ohne einen DOM-Baum zu erstellen - CSS-Selektoren — Abfrage von Elementen mit vertrauter CSS-Syntax (
css::select) inklusive Kombinatoren, Pseudoklassen und schnellem#id-Lookup - XPath 1.0+ — vollständiger Ausdrucksparser und -auswerter mit allen Kernfunktionen von XPath 1.0 sowie wichtigen XPath-2.0-Funktionen (
matches(),replace(),tokenize(),upper-case(),lower-case(),abs(),min(),max()und mehr) - Validierung — DTD-, RelaxNG-, XML-Schema-(XSD)- und ISO-Schematron-Validierung (ISO/IEC 19757-3)
- Serde-Integration — optionales
serde-Feature für XML-(De-)Serialisierung von/nach Rust-Typen - Asynchrones Parsen — optionales
async-Feature zum Parsen vontokio::io::AsyncRead-Quellen - Kanonisches XML — C14N 1.0 und Exclusive-C14N-Serialisierung
- XInclude — Dokumenteninklusionsverarbeitung
- XML-Kataloge — OASIS-XML-Kataloge zur URI-Auflösung
xmllint-CLI — Kommandozeilenwerkzeug zum Parsen, Validieren und Abfragen von XML- Zero-Copy wo möglich — String-Interning für schnelle Vergleiche
- Kein globaler Zustand — jedes
Documentist in sich abgeschlossen undSend + Sync - C/C++-FFI — vollständige C-API mit Header-Datei (
include/xmloxide.h) zur Einbettung in C/C++-Projekte - Minimale Abhängigkeiten — nur
encoding_rs(die Bibliothek hat keine weiteren Abhängigkeiten;clapist nur CLI-spezifisch)
Schnellstart
use xmloxide::Document;
let doc = Document::parse_str("<root><child>Hello</child></root>").unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.node_name(root), Some("root"));
assert_eq!(doc.text_content(root), "Hello");
Serialisierung
use xmloxide::Document;
use xmloxide::serial::serialize;
let doc = Document::parse_str("<root><child>Hello</child></root>").unwrap();
let xml = serialize(&doc);
assert_eq!(xml, "<root><child>Hello</child></root>");
XPath-Abfragen
use xmloxide::Document;
use xmloxide::xpath::{evaluate, XPathValue};
let doc = Document::parse_str("<library><book><title>Rust</title></book></library>").unwrap();
let root = doc.root_element().unwrap();
let result = evaluate(&doc, root, "count(book)").unwrap();
assert_eq!(result.to_number(), 1.0);
SAX2-Streaming
use xmloxide::sax::{parse_sax, SaxHandler, DefaultHandler};
use xmloxide::parser::ParseOptions;
struct MyHandler;
impl SaxHandler for MyHandler {
fn start_element(&mut self, name: &str, _: Option<&str>, _: Option<&str>,
_: &[(String, String, Option<String>, Option<String>)]) {
println!("Element: {name}");
}
}
parse_sax("<root><child/></root>", &ParseOptions::default(), &mut MyHandler).unwrap();
HTML-Parsing
use xmloxide::html::parse_html;
let doc = parse_html("<p>Hello <br> World").unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.node_name(root), Some("html"));
CSS-Selektoren
use xmloxide::css::select;
use xmloxide::Document;
let doc = Document::parse_str(r#"<div><p class="intro">Hello</p><p>World</p></div>"#).unwrap();
let root = doc.root_element().unwrap();
let intros = select(&doc, root, "p.intro").unwrap();
assert_eq!(intros.len(), 1);
assert_eq!(doc.text_content(intros[0]), "Hello");
HTML5-Parsing (WHATWG)
use xmloxide::html5::parse_html5;
let doc = parse_html5("<p>Hello <b>world</b>").unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.node_name(root), Some("html"));
Fragment-Parsing (der Algorithmus hinter innerHTML) wird ebenfalls unterstützt:
use xmloxide::html5::{parse_html5_with_options, Html5ParseOptions};
let opts = Html5ParseOptions {
scripting: false,
fragment_context: Some("body".to_string()),
};
let doc = parse_html5_with_options("<p>fragment</p>", &opts).unwrap();
HTML5-Streaming (SAX-ähnlich)
use xmloxide::html5::sax::{Html5SaxHandler, parse_html5_sax};
struct LinkExtractor { hrefs: Vec<String> }
impl Html5SaxHandler for LinkExtractor {
fn start_element(&mut self, name: &str, attrs: &[(String, String)], _sc: bool) {
if name == "a" {
if let Some((_, href)) = attrs.iter().find(|(n, _)| n == "href") {
self.hrefs.push(href.clone());
}
}
}
}
let mut handler = LinkExtractor { hrefs: Vec::new() };
parse_html5_sax(r#"<a href="/page">Link</a>"#, &mut handler);
assert_eq!(handler.hrefs, vec!["/page"]);
Fehlerbehandlung
use xmloxide::parser::{parse_str_with_options, ParseOptions};
let opts = ParseOptions::default().recover(true);
let doc = parse_str_with_options("<root><unclosed>", &opts).unwrap();
for diag in &doc.diagnostics {
eprintln!("{}", diag);
}
CLI-Werkzeug
# Analysieren und hübsch ausgeben
xmllint --format document.xml
# Gegen ein Schema validieren
xmllint --schema schema.xsd document.xml
xmllint --relaxng schema.rng document.xml
xmllint --schematron schema.sch document.xml
xmllint --dtdvalid schema.dtd document.xml
# XPath-Abfrage
xmllint --xpath "//title" document.xml
# Kanonisches XML
xmllint --c14n document.xml
# HTML parsen
xmllint --html page.html
Modulübersicht
| Modul | Beschreibung |
|---|---|
tree | Arena-basierter DOM-Baum (Document, NodeId, NodeKind) |
parser | rekursiv-absteigender XML-1.0-Parser mit Fehlerbehandlung |
parser::push | Push/inkrementeller Parser für stückweise Eingabe |
html | fehlertoleranter HTML-4.01-Parser |
html5 | WHATWG-HTML-Living-Standard-Parser (Tokenizer + Baumkonstruktor) |
html5::sax | Streaming-SAXX-ähnliche API für HTML5 (kein DOM-Baum erstellt) |
css | CSS-Selektor-Engine zum Abfragen von Dokumentbäumen |
sax | SAX2-Streaming-Ereignis-gesteuerter Parser |
reader | XmlReader-Pull-basierte Parsing-API |
serial | XML-, HTML- und HTML5-Serialisierer, plus kanonisches XML (C14N) |
xpath | XPath-1.0+-Ausdrucksparser und -auswerter |
validation::dtd | DTD-Parsing und -Validierung |
validation::relaxng | RelaxNG-Schemavalidierung |
validation::xsd | XML-Schema-(XSD)-Validierung |
validation::schematron | ISO-Schematron-regelbasierte Validierung |
serde_xml | Serde-XML-(De-)Serialisierung (optionales serde-Feature) |
async_xml | Asynchrones Parsen über tokio::io::AsyncRead (optionales async-Feature) |
xinclude | XInclude-1.0-Dokumenteninklusion |
catalog | OASIS-XML-Kataloge zur URI-Auflösung |
encoding | Zeichenkodierungserkennung und -transkodierung |
ffi | C/C++-FFI-Bindungen (include/xmloxide.h) |
Leistung
Der Parsing-Durchsatz ist vergleichbar mit libxml2 — innerhalb von 3–4% bei den meisten Dokumenten und 12 % schneller bei SVG. Die Serialisierung ist 1,5–2,4x schneller dank des arena-basierten Baumdesigns. XPath ist 1,1–2,7x schneller in allen Benchmarks.
Parsing:
| Dokument | Größe | xmloxide | libxml2 | Ergebnis |
|---|---|---|---|---|
| Atom-Feed | 4,9 KB | 26,7 µs (176 MiB/s) | 25,5 µs (184 MiB/s) | ~4 % langsamer |
| SVG-Zeichnung | 6,3 KB | 58,5 µs (103 MiB/s) | 65,6 µs (92 MiB/s) | 12 % schneller |
| Maven-POM | 11,5 KB | 76,9 µs (142 MiB/s) | 74,2 µs (148 MiB/s) | ~4 % langsamer |
| XHTML-Seite | 10,2 KB | 69,5 µs (139 MiB/s) | 61,5 µs (157 MiB/s) | ~13 % langsamer |
| Groß (374 KB) | 374 KB | 2,15 ms (169 MiB/s) | 2,08 ms (175 MiB/s) | ~3 % langsamer |
Serialisierung:
| Dokument | Größe | xmloxide | libxml2 | Ergebnis |
|---|---|---|---|---|
| Atom-Feed | 4,9 KB | 11,3 µs | 17,5 µs | 1,5x schneller |
| Maven-POM | 11,5 KB | 20,1 µs | 47,5 µs | 2,4x schneller |
| Groß (374 KB) | 374 KB | 614 µs | 1397 µs | 2,3x schneller |
XPath:
| Ausdruck | xmloxide | libxml2 | Ergebnis |
|---|---|---|---|
Einfacher Pfad (//entry/title) | 1,51 µs | 1,63 µs | 8 % schneller |
Attribut-Prädikat (//book[@id]) | 5,91 µs | 15,99 µs | 2,7x schneller |
count()-Funktion | 1,09 µs | 1,67 µs | 1,5x schneller |
string()-Funktion | 1,32 µs | 1,77 µs | 1,3x schneller |
Wichtige Optimierungen: Arena-basierter Baum für schnelle Serialisierung, Byte-Level-Vorprüfungen für Zeichenvalidierung, Bulk-Text-Scanning, ASCII-Schnellpfade für Namensparsing, Zero-Copy-Elementnamen-Aufteilung, Inline-Entity-Auflösung, XPath-//-Schrittfusion mit fusionierter Achsenerweiterung, Inline-Baumzugriffe und Namens-Test-Schnellpfade für Kind-/Nachkommenachsen.
# Benchmarks ausführen (erfordert libxml2-Systembibliothek)
cargo bench --features bench-libxml2 --bench comparison_bench
Tests
- 1078 Komponententests über alle Module hinweg
- 138 FFI-Tests decken die gesamte C-API-Oberfläche ab (einschließlich SAX, Schematron und CSS)
- libxml2-Kompatibilitätssuite — 119/119 Tests bestanden (100 %) für XML-Parsing, Namensräume, Fehlererkennung und HTML-Parsing
- W3C-XML-Konformitätstestsuite — 1727/1727 anwendbare Tests bestanden (100 %)
- html5lib-Tests — 7032/7032 Tokenizer-Tests + 1778/1778 Baumkonstruktionstests (100 %)
- Integrationstests decken reale XML/HTML-Dokumente, Randfälle und Fehlerbehandlung ab
cargo test --all-features
C/C++-FFI
xmloxide bietet eine C-kompatible API zur Einbettung in C/C++-Projekte (wie Chromium, Game-Engines oder jede Codebasis, die derzeit libxml2 verwendet).
# Gemeinsame + statische Bibliotheken bauen (verwendet das beiliegende Makefile)
make
# Oder einzeln bauen:
make shared # .so / .dylib / .dll
make static # .a / .lib
# C-Beispiel bauen und ausführen
make example
#include "xmloxide.h"
xmloxide_document *doc = xmloxide_parse_str("<root>Hello</root>");
uint32_t root = xmloxide_doc_root_element(doc);
char *name = xmloxide_node_name(doc, root); // "root"
char *text = xmloxide_node_text_content(doc, root); // "Hello"
xmloxide_free_string(name);
xmloxide_free_string(text);
xmloxide_free_doc(doc);
Die vollständige API — einschließlich Baum-Navigation und -Mutation, XPath-Auswertung, Serialisierung (einfach und hübsch gedruckt), HTML/HTML5-Parsing, DTD/RelaxNG/XSD/Schematron-Validierung, C14N, SAX-Streaming, XmlReader, Push-Parser und XML-Kataloge — ist in include/xmloxide.h deklariert.
Migration von libxml2
| libxml2 | xmloxide (Rust) | xmloxide (C FFI) |
|---|---|---|
xmlReadMemory | Document::parse_str | xmloxide_parse_str |
xmlReadFile | Document::parse_file | xmloxide_parse_file |
xmlParseDoc | Document::parse_bytes | xmloxide_parse_bytes |
htmlReadMemory | html::parse_html | xmloxide_parse_html |
| (HTML5-Parsing) | html5::parse_html5 | — |
| (HTML5-Fragment / innerHTML) | html5::parse_html5_with_options | — |
| (HTML5-Streaming) | html5::sax::parse_html5_sax | — |
| (CSS-Selektoren / querySelector) | css::select | — |
xmlFreeDoc | (drop Document) | xmloxide_free_doc |
xmlDocGetRootElement | doc.root_element() | xmloxide_doc_root_element |
xmlNodeGetContent | doc.text_content(id) | xmloxide_node_text_content |
xmlNodeSetContent | doc.set_text_content(id, s) | xmloxide_set_text_content |
xmlGetProp | doc.attribute(id, name) | xmloxide_node_attribute |
xmlSetProp | doc.set_attribute(...) | xmloxide_set_attribute |
xmlNewNode | doc.create_node(...) | xmloxide_create_element |
xmlNewText | doc.create_node(Text{..}) | xmloxide_create_text |
xmlAddChild | doc.append_child(p, c) | xmloxide_append_child |
xmlAddPrevSibling | doc.insert_before(ref, c) | xmloxide_insert_before |
xmlUnlinkNode | doc.remove_node(id) | xmloxide_remove_node |
xmlCopyNode | doc.clone_node(id, deep) | xmloxide_clone_node |
xmlGetID | doc.element_by_id(s) | xmloxide_element_by_id |
xmlDocDumpMemory | serial::serialize(&doc) | xmloxide_serialize |
xmlDocDumpFormatMemory | serial::serialize_with_options | xmloxide_serialize_pretty |
htmlDocDumpMemory | serial::html::serialize_html | xmloxide_serialize_html |
xmlC14NDocDumpMemory | serial::c14n::canonicalize | xmloxide_canonicalize |
xmlXPathEvalExpression | xpath::evaluate | xmloxide_xpath_eval |
xmlValidateDtd | validation::dtd::validate | xmloxide_validate_dtd |
xmlRelaxNGValidateDoc | validation::relaxng::validate | xmloxide_validate_relaxng |
xmlSchemaValidateDoc | validation::xsd::validate_xsd | xmloxide_validate_xsd |
| (Schematron-Validierung) | validation::schematron::validate_schematron | xmloxide_validate_schematron |
xmlXIncludeProcess | xinclude::process_xincludes | xmloxide_process_xincludes |
xmlLoadCatalog | Catalog::parse | xmloxide_parse_catalog |
xmlSAX2...-Callbacks | sax::SaxHandler-Trait | xmloxide_sax_parse |
xmlTextReaderRead | reader::XmlReader | xmloxide_reader_read |
xmlCreatePushParserCtxt | parser::PushParser | xmloxide_push_parser_new |
xmlParseChunk | PushParser::push | xmloxide_push_parser_push |
Thread-Sicherheit: Im Gegensatz zu libxml2 hat xmloxide keinen globalen Zustand. Jedes Document ist in sich abgeschlossen und Send + Sync. Die FFI-Schicht verwendet thread-lokalen Speicher für die letzte Fehlermeldung – jeder Thread hat seinen eigenen Fehlerzustand. Es werden keine Initialisierungs- oder Bereinigungsfunktionen benötigt.
Fuzzing
xmloxide enthält Fuzz-Ziele für Sicherheitstests:
# cargo-fuzz installieren (erfordert nightly)
cargo install cargo-fuzz
# Ein Fuzz-Ziel ausführen
cargo +nightly fuzz run fuzz_xml_parse
cargo +nightly fuzz run fuzz_html_parse
cargo +nightly fuzz run fuzz_html5_parse
cargo +nightly fuzz run fuzz_html5_fragment
cargo +nightly fuzz run fuzz_xpath
cargo +nightly fuzz run fuzz_roundtrip
cargo +nightly fuzz run fuzz_sax
cargo +nightly fuzz run fuzz_reader
cargo +nightly fuzz run fuzz_push
cargo +nightly fuzz run fuzz_validation
cargo +nightly fuzz run fuzz_schematron
Bauen
cargo build
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo bench
Mindestunterstützte Rust-Version: 1.81
Einschränkungen
- Kein XML 1.1 — xmloxide implementiert nur XML 1.0 (Fünfte Edition). XML 1.1 wird selten verwendet und ist nicht geplant.
- Kein XSLT — XSLT ist eine separate Spezifikation (libxslt) und außerhalb des Rahmens.
- HTML-Parser — Es werden sowohl ein HTML-4.01-Parser (der das Verhalten von libxml2 nachbildet) als auch ein vollständiger WHATWG-HTML5-Parser bereitgestellt. Der HTML5-Parser besteht 100% der html5lib-Tests.
- Push-Parser puffert intern — die Push/inkrementelle Parser-API (
PushParser) puffert derzeit alle gepushten Daten und führt die vollständige Analyse beifinish()durch, anstatt wirklich zu streamen wie libxml2sxmlParseChunk. SAX-Streaming (parse_saxfür XML,html5::sax::parse_html5_saxfür HTML5) steht als Alternative für die speicherbeschränkte Verarbeitung großer Dokumente zur Verfügung. - XPath-
namespace::-Achse — dienamespace::-Achse gibt den Elementknoten zurück, wenn Namensräume im Gültigkeitsbereich übereinstimmen (anstatt separate Namensraumknoten zu materialisieren), und folgt damit dem gleichen Muster wie die Attributachse.
Mitwirken
Siehe CONTRIBUTING.md für Entwicklungseinrichtung und Richtlinien.
Changelog
Siehe CHANGELOG.md für den Versionsverlauf.
Lizenz
MIT