
xmloxide v0.5.0
Une réimplémentation en pur Rust de libxml2
xmloxide
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é.
Fonctionnalités
- Sécurité mémoire — arbre basé sur un arena avec zéro
unsafedans l'API publique - Conforme — taux de réussite de 100 % sur la suite de tests de conformité XML du W3C (1727/1727 tests applicables)
- Récupération d'erreurs — analyser du XML mal formé et quand même produire un arbre utilisable, tout comme libxml2
- Plusieurs API d'analyse — arbre DOM, streaming SAX2, pull XmlReader, push/incrémentale
- Analyseur HTML — analyse HTML 4.01 tolérante aux erreurs avec fermeture automatique et éléments vides
- Analyseur HTML5 WHATWG — tokeniseur et constructeur d'arbre complets du HTML Living Standard (8810/8810 tests html5lib-tests réussis)
- Streaming HTML5 — API de rappel de type SAX pour HTML5 (
html5::sax) qui encapsule le tokeniseur sans construire d'arbre DOM - Sélecteurs CSS — interroger des éléments avec une syntaxe CSS familière (
css::select) incluant combinateurs, pseudo-classes et recherche rapide par#id - XPath 1.0+ — analyseur et évaluateur d'expressions complet avec toutes les fonctions de base XPath 1.0 plus des fonctions clés XPath 2.0 (
matches(),replace(),tokenize(),upper-case(),lower-case(),abs(),min(),max(), et plus) - Validation — validation DTD, RelaxNG, Schéma XML (XSD) et Schematron ISO (ISO/IEC 19757-3)
- Intégration Serde — fonctionnalité
serdeoptionnelle pour la (dé)sérialisation XML vers/depuis des types Rust - Analyse asynchrone — fonctionnalité
asyncoptionnelle pour analyser à partir de sourcestokio::io::AsyncRead - XML canonique — sérialisation C14N 1.0 et C14N exclusive
- XInclude — traitement d'inclusion de documents
- Catalogues XML — catalogues XML OASIS pour la résolution d'URI
- CLI
xmllint— outil en ligne de commande pour analyser, valider et interroger du XML - Zéro copie quand c'est possible — internement de chaînes pour des comparaisons rapides
- Aucun état global — chaque
Documentest autonome etSend + Sync - FFI C/C++ — API C complète avec fichier d'en-tête (
include/xmloxide.h) pour intégration dans des projets C/C++ - Dépendances minimales — seulement
encoding_rs(la bibliothèque n'a aucune autre dépendance ;clapest uniquement CLI)
Démarrage rapide
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");
Sérialisation
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>");
Requêtes XPath
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);
Streaming SAX2
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();
Analyse HTML
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"));
Sélecteurs CSS
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");
Analyse HTML5 (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"));
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();
Streaming HTML5 (type SAX)
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"]);
Récupération d'erreurs
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);
}
Outil CLI
# 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
Aperçu des modules
| 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) |
Performances
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 :
| 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 |
Sérialisation :
| 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 |
XPath :
| 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 |
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
Tests
- 1078 tests unitaires dans tous les modules
- 138 tests FFI couvrant toute la surface de l'API C (y compris SAX, Schematron et CSS)
- Suite de compatibilité libxml2 — 119/119 tests réussis (100 %) couvrant l'analyse XML, les espaces de noms, la détection d'erreurs et l'analyse HTML
- Suite de tests de conformité XML du W3C — 1727/1727 tests applicables réussis (100 %)
- html5lib-tests — 7032/7032 tests de tokeniseur + 1778/1778 tests de construction d'arbre (100 %)
- Tests d'intégration couvrant des documents XML/HTML réels, des cas limites et la récupération d'erreurs
cargo test --all-features
FFI C/C++
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.
Migration depuis libxml2
| 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 |
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.
Fuzzing
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
Construction
cargo build
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo bench
Version minimale de Rust prise en charge : 1.81
Limitations
- Pas de XML 1.1 — xmloxide implémente XML 1.0 (Cinquième Édition) uniquement. XML 1.1 est rarement utilisé et n'est pas prévu.
- Pas de XSLT — XSLT est une spécification distincte (libxslt) et est hors du champ d'application.
- Analyseurs HTML — un analyseur HTML 4.01 (correspondant au comportement de libxml2) et un analyseur HTML5 WHATWG complet sont fournis. L'analyseur HTML5 réussit 100 % des html5lib-tests.
- L'analyseur push met en mémoire tampon en interne — l'API d'analyse push/incrémentale (
PushParser) met actuellement en mémoire tampon toutes les données poussées et effectue l'analyse complète lors definish(), plutôt qu'un véritable streaming commexmlParseChunkde libxml2. Le streaming SAX (parse_saxpour XML,html5::sax::parse_html5_saxpour HTML5) est disponible comme alternative pour le traitement de grands documents avec mémoire limitée. - Axe XPath
namespace::— l'axenamespace::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.
Contribuer
Voir CONTRIBUTING.md pour la configuration de développement et les directives.
Journal des modifications
Voir CHANGELOG.md pour l'historique des versions.
Licence
MIT