
Eine reine Rust-Neuumsetzung von libxml2
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.
unsafe in der öffentlichen APIhtml5::sax), die den Tokenizer umschließt, ohne einen DOM-Baum zu erstellencss::select) inklusive Kombinatoren, Pseudoklassen und schnellem #id-Lookupmatches(), replace(), tokenize(), upper-case(), lower-case(), abs(), min(), max() und mehr)serde-Feature für XML-(De-)Serialisierung von/nach Rust-Typenasync-Feature zum Parsen von tokio::io::AsyncRead-Quellenxmllint-CLI — Kommandozeilenwerkzeug zum Parsen, Validieren und Abfragen von XMLDocument ist in sich abgeschlossen und Send + Syncinclude/xmloxide.h) zur Einbettung in C/C++-Projekteencoding_rs (die Bibliothek hat keine weiteren Abhängigkeiten; clap ist nur CLI-spezifisch)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");
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>");
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);
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();
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"));
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");
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();
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"]);
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);
}
# 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
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:
Serialisierung:
XPath:
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
cargo test --all-features
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.
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.
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
cargo build
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo bench
Mindestunterstützte Rust-Version: 1.81
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.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.Siehe CONTRIBUTING.md für Entwicklungseinrichtung und Richtlinien.
Siehe CHANGELOG.md für den Versionsverlauf.
MIT
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |