
xmloxide v0.5.0
Una reimplementación en Rust puro de libxml2
xmloxide
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.
Características
- Seguridad en memoria — árbol basado en arena con cero
unsafeen la API pública - Conformidad — 100% de tasa de aprobación en el W3C XML Conformance Test Suite (1727/1727 pruebas aplicables)
- Recuperación de errores — analiza XML malformado y aún produce un árbol utilizable, al igual que libxml2
- Múltiples API de análisis — árbol DOM, SAX2 streaming, XmlReader pull, push/incremental
- Analizador HTML — análisis HTML 4.01 tolerante a errores con auto-cierre y elementos vacíos
- Analizador HTML5 WHATWG — tokenizador y constructor de árbol completo del HTML Living Standard (8810/8810 pruebas html5lib-tests aprobadas)
- Streaming HTML5 — API de callback tipo SAX para HTML5 (
html5::sax) que envuelve el tokenizador sin construir un árbol DOM - Selectores CSS — consulta de elementos con sintaxis CSS familiar (
css::select) incluyendo combinadores, pseudo-clases y búsqueda rápida por#id - XPath 1.0+ — analizador y evaluador de expresiones completo con todas las funciones principales de XPath 1.0 más funciones clave de XPath 2.0 (
matches(),replace(),tokenize(),upper-case(),lower-case(),abs(),min(),max()y más) - Validación — validación DTD, RelaxNG, XML Schema (XSD) e ISO Schematron (ISO/IEC 19757-3)
- Integración con Serde — característica opcional
serdepara (de)serialización XML hacia/desde tipos de Rust - Análisis asíncrono — característica opcional
asyncpara analizar desde fuentestokio::io::AsyncRead - XML Canónico — serialización C14N 1.0 y C14N Exclusivo
- XInclude — procesamiento de inclusión de documentos
- Catálogos XML — Catálogos XML OASIS para resolución de URI
- CLI
xmllint— herramienta de línea de comandos para analizar, validar y consultar XML - Cero copia cuando es posible — internado de cadenas para comparaciones rápidas
- Sin estado global — cada
Documentes autocontenido ySend + Sync - FFI C/C++ — API C completa con archivo de cabecera (
include/xmloxide.h) para integración en proyectos C/C++ - Dependencias mínimas — solo
encoding_rs(la biblioteca no tiene otras dependencias;clapes solo para CLI)
Inicio rápido
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");
Serialización
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>");
Consultas 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);
SAX2 Streaming
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();
Análisis 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"));
Selectores 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");
Análisis 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"));
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();
Streaming HTML5 (tipo 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"]);
Recuperación de errores
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);
}
Herramienta CLI
# 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
Resumen de módulos
| 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) |
Rendimiento
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:
| 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 |
Serialización:
| 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 |
XPath:
| 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 |
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
Pruebas
- 1078 pruebas unitarias en todos los módulos
- 138 pruebas FFI que cubren toda la superficie de la API C (incluyendo SAX, Schematron y CSS)
- Suite de compatibilidad libxml2 — 119/119 pruebas aprobadas (100%) que cubren análisis XML, espacios de nombres, detección de errores y análisis HTML
- W3C XML Conformance Test Suite — 1727/1727 pruebas aplicables aprobadas (100%)
- html5lib-tests — 7032/7032 pruebas de tokenizador + 1778/1778 pruebas de construcción de árbol (100%)
- Pruebas de integración que cubren documentos XML/HTML del mundo real, casos límite y recuperación de errores
cargo test --all-features
FFI C/C++
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.
Migración desde 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 |
| (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 |
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.
Fuzzing
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
Compilación
cargo build
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo bench
Versión mínima de Rust soportada: 1.81
Limitaciones
- Sin XML 1.1 — xmloxide implementa solo XML 1.0 (Quinta Edición). XML 1.1 rara vez se usa y no está planificado.
- Sin XSLT — XSLT es una especificación separada (libxslt) y está fuera del alcance.
- Analizadores HTML — se proporcionan tanto un analizador HTML 4.01 (que coincide con el comportamiento de libxml2) como un analizador HTML5 WHATWG completo. El analizador HTML5 pasa el 100% de las pruebas html5lib-tests.
- El analizador push almacena en búfer internamente — la API del analizador push/incremental (
PushParser) actualmente almacena en búfer todos los datos enviados y realiza el análisis completo enfinish(), en lugar de transmitir verdaderamente comoxmlParseChunkde libxml2. El streaming SAX (parse_saxpara XML,html5::sax::parse_html5_saxpara HTML5) está disponible como alternativa para el procesamiento de documentos grandes con restricciones de memoria. - Eje
namespace::de XPath — el ejenamespace::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.
Contribuciones
Consulte CONTRIBUTING.md para la configuración de desarrollo y las pautas.
Registro de cambios
Consulte CHANGELOG.md para el historial de versiones.
Licencia
MIT