
Eine reine Rust-Neuumsetzung von libxml2
# xmloxide
[](https://github.com/jonwiggins/xmloxide/actions/workflows/ci.yml)
[](https://crates.io/crates/xmloxide)
[](https://docs.rs/xmloxide)
[](LICENSE)
[](https://www.rust-lang.org)
Eine reine Rust-Neuimplementierung von [libxml2](https://gitlab.gnome.org/GNOME/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 `unsafe` in 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](https://html.spec.whatwg.org/)-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 von `tokio::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 `Document` ist in sich abgeschlossen und `Send + 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; `clap` ist nur CLI-spezifisch)
## Schnellstart
```rust
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
```rust
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
```rust
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
```rust
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
```rust
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
```rust
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)
```rust
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:
```rust
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)
```rust
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="https://github.com/jonwiggins/xmloxide/blob/main/page">Link</a>"#, &mut handler);
assert_eq!(handler.hrefs, vec!["/page"]);
```
### Fehlerbehandlung
```rust
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
```sh
# 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.
```sh
# 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
```sh
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).
```sh
# 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
```
```c
#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`](https://github.com/jonwiggins/xmloxide/blob/main/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:
```sh
# 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
```sh
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 bei `finish()` durch, anstatt wirklich zu streamen wie libxml2s `xmlParseChunk`. SAX-Streaming (`parse_sax` für XML, `html5::sax::parse_html5_sax` für HTML5) steht als Alternative für die speicherbeschränkte Verarbeitung großer Dokumente zur Verfügung.
- **XPath-`namespace::`-Achse** — die `namespace::`-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](https://github.com/jonwiggins/xmloxide/blob/main/CONTRIBUTING.md) für Entwicklungseinrichtung und Richtlinien.
## Changelog
Siehe [CHANGELOG.md](https://github.com/jonwiggins/xmloxide/blob/main/CHANGELOG.md) für den Versionsverlauf.
## Lizenz
MIT