
Una reimplementación en Rust puro de libxml2
Una reimplementación pura en Rust de libxml2 — la biblioteca de análisis XML/HTML de facto en el mundo del código abierto.
libxml2 quedó oficialmente sin mantenimiento en diciembre de 2025 con problemas de seguridad conocidos. xmloxide aspira a ser un reemplazo seguro en memoria y de alto rendimiento que pase los mismos conjuntos de pruebas de conformidad.
unsafe en la API públicahtml5::sax) que envuelve el tokenizador sin construir un árbol DOMcss::select) incluyendo combinadores, pseudo-clases y búsqueda rápida por #idmatches(), replace(), tokenize(), upper-case(), lower-case(), abs(), min(), max() y más)serde para (de)serialización XML hacia/desde tipos de Rustasync para analizar desde fuentes tokio::io::AsyncReadxmllint — herramienta de línea de comandos para analizar, validar y consultar XMLDocument es autocontenido y Send + Syncinclude/xmloxide.h) para integración en proyectos C/C++encoding_rs (la biblioteca no tiene otras dependencias; clap es solo para 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"));
El análisis de fragmentos (el algoritmo detrás de innerHTML) también es compatible:
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);
}
# Analizar y embellecer
xmllint --format document.xml
# Validar contra un esquema
xmllint --schema schema.xsd document.xml
xmllint --relaxng schema.rng document.xml
xmllint --schematron schema.sch document.xml
xmllint --dtdvalid schema.dtd document.xml
# Consulta XPath
xmllint --xpath "//title" document.xml
# XML Canónico
xmllint --c14n document.xml
# Analizar HTML
xmllint --html page.html
El rendimiento de análisis es competitivo con libxml2 — dentro del 3-4% en la mayoría de los documentos, y 12% más rápido en SVG. La serialización es 1.5-2.4 veces más rápida gracias al diseño del árbol basado en arena. XPath es 1.1-2.7 veces más rápido en todos los benchmarks.
Análisis:
Serialización:
XPath:
Optimizaciones clave: árbol basado en arena para serialización rápida, pre-verificaciones a nivel de byte para validación de caracteres, escaneo de texto por lotes, rutas rápidas ASCII para análisis de nombres, división de nombres de elementos de cero copia, resolución de entidades en línea, fusión de pasos // XPath con expansión de ejes fusionados, accesores de árbol en línea y rutas rápidas de prueba de nombre para ejes hijo/descendiente.
# Ejecutar benchmarks (requiere la biblioteca del sistema libxml2)
cargo bench --features bench-libxml2 --bench comparison_bench
cargo test --all-features
xmloxide proporciona una API compatible con C para integración en proyectos C/C++ (como Chromium, motores de juegos o cualquier base de código que actualmente use libxml2).
# Compilar bibliotecas compartida + estática (usa el Makefile incluido)
make
# O compilar individualmente:
make shared # .so / .dylib / .dll
make static # .a / .lib
# Compilar y ejecutar el ejemplo en 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);
La API completa — incluyendo navegación y mutación de árbol, evaluación XPath, serialización (plana y embellecida), análisis HTML/HTML5, validación DTD/RelaxNG/XSD/Schematron, C14N, streaming SAX, XmlReader, analizador push y Catálogos XML — está declarada en include/xmloxide.h.
Seguridad de hilos: A diferencia de libxml2, xmloxide no tiene estado global. Cada Document es autocontenido y Send + Sync. La capa FFI utiliza almacenamiento local de hilo para el último mensaje de error — cada hilo tiene su propio estado de error. No se necesitan funciones de inicialización o limpieza.
xmloxide incluye objetivos de fuzzing para pruebas de seguridad:
# Instalar cargo-fuzz (requiere nightly)
cargo install cargo-fuzz
# Ejecutar un objetivo 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
Versión mínima de Rust soportada: 1.81
PushParser) actualmente almacena en búfer todos los datos enviados y realiza el análisis completo en finish(), en lugar de transmitir verdaderamente como xmlParseChunk de libxml2. El streaming SAX (parse_sax para XML, html5::sax::parse_html5_sax para HTML5) está disponible como alternativa para el procesamiento de documentos grandes con restricciones de memoria.namespace:: de XPath — el eje namespace:: devuelve el nodo del elemento cuando los espacios de nombres en alcance coinciden (en lugar de materializar nodos de espacio de nombres separados), siguiendo el mismo patrón que el eje de atributos.Consulte CONTRIBUTING.md para la configuración de desarrollo y las pautas.
Consulte CHANGELOG.md para el historial de versiones.
MIT
| Módulo | Descripción |
|---|
tree | Árbol DOM basado en arena (Document, NodeId, NodeKind) |
parser | Analizador descendente recursivo XML 1.0 con recuperación de errores |
parser::push | Analizador push/incremental para entrada fragmentada |
html | Analizador HTML 4.01 tolerante a errores |
html5 | Analizador HTML Living Standard WHATWG (tokenizador + constructor de árbol) |
html5::sax | API de streaming tipo SAX para HTML5 (sin construcción de árbol DOM) |
css | Motor de selectores CSS para consultar árboles de documentos |
sax | Analizador SAX2 basado en eventos de streaming |
reader | API de análisis basado en pull XmlReader |
serial | Serializadores XML, HTML y HTML5, más XML Canónico (C14N) |
xpath | Analizador y evaluador de expresiones XPath 1.0+ |
validation::dtd | Análisis y validación DTD |
validation::relaxng | Validación de esquemas RelaxNG |
validation::xsd | Validación de esquemas XML (XSD) |
validation::schematron | Validación basada en reglas ISO Schematron |
serde_xml | (Des)serialización Serde XML (característica opcional serde) |
async_xml | Análisis asíncrono mediante tokio::io::AsyncRead (característica opcional async) |
xinclude | Inclusión de documentos XInclude 1.0 |
catalog | Catálogos XML OASIS para resolución de URI |
encoding | Detección de codificación de caracteres y transcodificación |
ffi | Enlaces FFI C/C++ (include/xmloxide.h) |
| Documento | Tamaño | xmloxide | libxml2 | Resultado |
|---|
| Feed Atom | 4.9 KB | 26,7 µs (176 MiB/s) | 25,5 µs (184 MiB/s) | ~4% más lento |
| Dibujo SVG | 6.3 KB | 58,5 µs (103 MiB/s) | 65,6 µs (92 MiB/s) | 12% más rápido |
| POM de Maven | 11.5 KB | 76,9 µs (142 MiB/s) | 74,2 µs (148 MiB/s) | ~4% más lento |
| Página XHTML | 10.2 KB | 69,5 µs (139 MiB/s) | 61,5 µs (157 MiB/s) | ~13% más lento |
| Grande (374 KB) | 374 KB | 2,15 ms (169 MiB/s) | 2,08 ms (175 MiB/s) | ~3% más lento |
| Documento | Tamaño | xmloxide | libxml2 | Resultado |
|---|
| Feed Atom | 4.9 KB | 11,3 µs | 17,5 µs | 1,5x más rápido |
| POM de Maven | 11.5 KB | 20,1 µs | 47,5 µs | 2,4x más rápido |
| Grande (374 KB) | 374 KB | 614 µs | 1397 µs | 2,3x más rápido |
| Expresión | xmloxide | libxml2 | Resultado |
|---|
Ruta simple (//entry/title) | 1,51 µs | 1,63 µs | 8% más rápido |
Predicado de atributo (//book[@id]) | 5,91 µs | 15,99 µs | 2,7x más rápido |
Función count() | 1,09 µs | 1,67 µs | 1,5x más rápido |
Función string() | 1,32 µs | 1,77 µs | 1,3x más rápido |
| 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 |
| (Análisis HTML5) | html5::parse_html5 | — |
| (Fragmento HTML5 / innerHTML) | html5::parse_html5_with_options | — |
| (Streaming HTML5) | html5::sax::parse_html5_sax | — |
(Selectores CSS / querySelector) | css::select | — |
xmlFreeDoc | (liberar 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 |
| (Validación 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 |