
Une réimplémentation en pur Rust de libxml2
Une réimplémentation pure en Rust de libxml2 — la bibliothèque d'analyse XML/HTML de facto standard dans le monde open-source.
libxml2 est officiellement devenu non maintenu en décembre 2025 avec des problèmes de sécurité connus. xmloxide vise à être un remplacement sûr en mémoire et haute performance qui réussit les mêmes suites de tests de conformité.
unsafe dans l'API publiquehtml5::sax) qui encapsule le tokeniseur sans construire d'arbre DOMcss::select) incluant combinateurs, pseudo-classes et recherche rapide par #idmatches(), replace(), tokenize(), upper-case(), lower-case(), abs(), min(), max(), et plus)serde optionnelle pour la (dé)sérialisation XML vers/depuis des types Rustasync optionnelle pour analyser à partir de sources tokio::io::AsyncReadxmllint — outil en ligne de commande pour analyser, valider et interroger du XMLDocument est autonome et Send + Syncinclude/xmloxide.h) pour intégration dans des projets C/C++encoding_rs (la bibliothèque n'a aucune autre dépendance ; clap est uniquement CLI)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"));
L'analyse de fragments (l'algorithme derrière innerHTML) est également prise en charge :
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);
}
# Analyser et afficher avec une mise en forme
xmllint --format document.xml
# Valider par rapport à un schéma
xmllint --schema schema.xsd document.xml
xmllint --relaxng schema.rng document.xml
xmllint --schematron schema.sch document.xml
xmllint --dtdvalid schema.dtd document.xml
# Requête XPath
xmllint --xpath "//title" document.xml
# XML canonique
xmllint --c14n document.xml
# Analyser HTML
xmllint --html page.html
Le débit d'analyse est compétitif avec libxml2 — à moins de 3–4 % sur la plupart des documents, et 12 % plus rapide sur SVG. La sérialisation est 1,5 à 2,4 fois plus rapide grâce à la conception de l'arbre basé sur arena. XPath est 1,1 à 2,7 fois plus rapide sur tous les benchmarks.
Analyse :
Sérialisation :
XPath :
Optimisations clés : arbre basé sur arena pour une sérialisation rapide, pré-vérifications au niveau des octets pour la validation des caractères, analyse en bloc du texte, chemins rapides ASCII pour l'analyse des noms, séparation de noms d'éléments sans copie, résolution d'entités en ligne, fusion d'étapes XPath // avec expansion d'axes fusionnés, accesseurs d'arbre inlinés, et chemins rapides de test de noms pour les axes enfants/descendants.
# Exécuter les benchmarks (nécessite la bibliothèque système libxml2)
cargo bench --features bench-libxml2 --bench comparison_bench
cargo test --all-features
xmloxide fournit une API compatible C pour l'intégration dans des projets C/C++ (comme Chromium, les moteurs de jeu, ou toute base de code utilisant actuellement libxml2).
# Construire les bibliothèques partagées + statiques (utilise le Makefile inclus)
make
# Ou construire individuellement :
make shared # .so / .dylib / .dll
make static # .a / .lib
# Construire et exécuter l'exemple C
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);
L'API complète — incluant la navigation et la mutation de l'arbre, l'évaluation XPath, la sérialisation (simple et avec mise en forme), l'analyse HTML/HTML5, la validation DTD/RelaxNG/XSD/Schematron, C14N, le streaming SAX, XmlReader, l'analyseur push et les catalogues XML — est déclarée dans include/xmloxide.h.
Sécurité des threads : Contrairement à libxml2, xmloxide n'a pas d'état global. Chaque Document est autonome et Send + Sync. La couche FFI utilise un stockage local au thread pour le dernier message d'erreur — chaque thread a son propre état d'erreur. Aucune fonction d'initialisation ou de nettoyage n'est nécessaire.
xmloxide inclut des cibles de fuzzing pour les tests de sécurité :
# Installer cargo-fuzz (nécessite nightly)
cargo install cargo-fuzz
# Exécuter une cible de fuzzing
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
Version minimale de Rust prise en charge : 1.81
PushParser) met actuellement en mémoire tampon toutes les données poussées et effectue l'analyse complète lors de finish(), plutôt qu'un véritable streaming comme xmlParseChunk de libxml2. Le streaming SAX (parse_sax pour XML, html5::sax::parse_html5_sax pour HTML5) est disponible comme alternative pour le traitement de grands documents avec mémoire limitée.namespace:: — l'axe namespace:: renvoie le nœud élément lorsque les espaces de noms en portée correspondent (plutôt que de matérialiser des nœuds d'espace de noms séparés), suivant le même modèle que l'axe d'attribut.Voir CONTRIBUTING.md pour la configuration de développement et les directives.
Voir CHANGELOG.md pour l'historique des versions.
MIT
| Module | Description |
|---|
tree | Arbre DOM basé sur arena (Document, NodeId, NodeKind) |
parser | Analyseur récursif descendant XML 1.0 avec récupération d'erreurs |
parser::push | Analyseur push/incrémentale pour entrée par morceaux |
html | Analyseur HTML 4.01 tolérant aux erreurs |
html5 | Analyseur WHATWG HTML Living Standard (tokeniseur + constructeur d'arbre) |
html5::sax | API streaming de type SAX pour HTML5 (pas d'arbre DOM construit) |
css | Moteur de sélecteurs CSS pour interroger les arbres de documents |
sax | Analyseur événementiel streaming SAX2 |
reader | API d'analyse par pull XmlReader |
serial | Sérialiseurs XML, HTML et HTML5, plus XML canonique (C14N) |
xpath | Analyseur et évaluateur d'expressions XPath 1.0+ |
validation::dtd | Analyse et validation DTD |
validation::relaxng | Validation de schéma RelaxNG |
validation::xsd | Validation de schéma XML (XSD) |
validation::schematron | Validation basée sur des règles ISO Schematron |
serde_xml | (Dé)sérialisation Serde XML (fonctionnalité serde optionnelle) |
async_xml | Analyse asynchrone via tokio::io::AsyncRead (fonctionnalité async optionnelle) |
xinclude | Inclusion de documents XInclude 1.0 |
catalog | Catalogues XML OASIS pour la résolution d'URI |
encoding | Détection et transcodage d'encodage de caractères |
ffi | Liaisons FFI C/C++ (include/xmloxide.h) |
| Document | Taille | xmloxide | libxml2 | Résultat |
|---|
| Flux Atom | 4,9 Ko | 26,7 µs (176 Mio/s) | 25,5 µs (184 Mio/s) | ~4 % plus lent |
| Dessin SVG | 6,3 Ko | 58,5 µs (103 Mio/s) | 65,6 µs (92 Mio/s) | 12 % plus rapide |
| POM Maven | 11,5 Ko | 76,9 µs (142 Mio/s) | 74,2 µs (148 Mio/s) | ~4 % plus lent |
| Page XHTML | 10,2 Ko | 69,5 µs (139 Mio/s) | 61,5 µs (157 Mio/s) | ~13 % plus lent |
| Grand (374 Ko) | 374 Ko | 2,15 ms (169 Mio/s) | 2,08 ms (175 Mio/s) | ~3 % plus lent |
| Document | Taille | xmloxide | libxml2 | Résultat |
|---|
| Flux Atom | 4,9 Ko | 11,3 µs | 17,5 µs | 1,5 fois plus rapide |
| POM Maven | 11,5 Ko | 20,1 µs | 47,5 µs | 2,4 fois plus rapide |
| Grand (374 Ko) | 374 Ko | 614 µs | 1397 µs | 2,3 fois plus rapide |
| Expression | xmloxide | libxml2 | Résultat |
|---|
Chemin simple (//entry/title) | 1,51 µs | 1,63 µs | 8 % plus rapide |
Prédicat d'attribut (//book[@id]) | 5,91 µs | 15,99 µs | 2,7 fois plus rapide |
Fonction count() | 1,09 µs | 1,67 µs | 1,5 fois plus rapide |
Fonction string() | 1,32 µs | 1,77 µs | 1,3 fois plus rapide |
| libxml2 | xmloxide (Rust) | xmloxide (FFI C) |
|---|
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 |
| (analyse HTML5) | html5::parse_html5 | — |
| (fragment HTML5 / innerHTML) | html5::parse_html5_with_options | — |
| (streaming HTML5) | html5::sax::parse_html5_sax | — |
(sélecteurs CSS / querySelector) | css::select | — |
xmlFreeDoc | (libérer 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 |
| (validation Schematron) | validation::schematron::validate_schematron | xmloxide_validate_schematron |
xmlXIncludeProcess | xinclude::process_xincludes | xmloxide_process_xincludes |
xmlLoadCatalog | Catalog::parse | xmloxide_parse_catalog |
callbacks xmlSAX2... | trait sax::SaxHandler | xmloxide_sax_parse |
xmlTextReaderRead | reader::XmlReader | xmloxide_reader_read |
xmlCreatePushParserCtxt | parser::PushParser | xmloxide_push_parser_new |
xmlParseChunk | PushParser::push | xmloxide_push_parser_push |