
Uma reimplementação pura em Rust do libxml2
Uma reimplementação pura em Rust do libxml2 — a biblioteca de análise XML/HTML padrão de facto no mundo open-source.
O libxml2 tornou-se oficialmente não mantido em dezembro de 2025, com problemas de segurança conhecidos. O xmloxide visa ser um substituto seguro em termos de memória e de alto desempenho que passa nos mesmos conjuntos de testes de conformidade.
unsafe na API públicahtml5::sax) que encapsula o tokenizer sem construir uma árvore DOMcss::select) incluindo combinadores, pseudo-classes e pesquisa rápida por #idmatches(), replace(), tokenize(), upper-case(), lower-case(), abs(), min(), max() e mais)serde para (de)serialização XML de/para tipos Rustasync para análise a partir de fontes tokio::io::AsyncReadxmllint — ferramenta de linha de comando para analisar, validar e consultar XMLDocument é autónomo e Send + Syncinclude/xmloxide.h) para incorporação em projetos C/C++encoding_rs (a biblioteca não tem outras dependências; clap é apenas 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!("Elemento: {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"));
A análise de fragmentos (o algoritmo por detrás do innerHTML) também é suportada:
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);
}
# Analisar e imprimir formatado
xmllint --format document.xml
# Validar contra um 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
# Analisar HTML
xmllint --html page.html
A taxa de transferência de análise é competitiva com o libxml2 — dentro de 3-4% na maioria dos documentos, e 12% mais rápida em SVG. A serialização é 1,5-2,4x mais rápida graças ao design da árvore baseada em arena. O XPath é 1,1-2,7x mais rápido em todos os benchmarks.
Análise:
Serialização:
XPath:
Otimizações principais: árvore baseada em arena para serialização rápida, pré-verificações ao nível do byte para validação de caracteres, leitura em massa de texto, caminhos rápidos ASCII para análise de nomes, divisão de nomes de elementos sem cópia, resolução inline de entidades, fusão de passos // do XPath com expansão de eixos fundidos, acessores de árvore inline e caminhos rápidos de teste de nome para eixos child/descendant.
# Executar benchmarks (requer biblioteca de sistema libxml2)
cargo bench --features bench-libxml2 --bench comparison_bench
cargo test --all-features
O xmloxide fornece uma API compatível com C para incorporação em projetos C/C++ (como Chromium, motores de jogo ou qualquer base de código que atualmente use libxml2).
# Construir bibliotecas partilhada + estática (usa o Makefile incluído)
make
# Ou construir individualmente:
make shared # .so / .dylib / .dll
make static # .a / .lib
# Construir e executar o exemplo em 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);
A API completa — incluindo navegação e mutação de árvore, avaliação XPath, serialização (simples e formatada), análise HTML/HTML5, validação DTD/RelaxNG/XSD/Schematron, C14N, streaming SAX, XmlReader, analisador push e Catálogos XML — é declarada em include/xmloxide.h.
Segurança em threads: Ao contrário do libxml2, o xmloxide não possui estado global. Cada Document é autónomo e Send + Sync. A camada FFI utiliza armazenamento local de thread para a última mensagem de erro — cada thread tem o seu próprio estado de erro. Nenhuma função de inicialização ou limpeza é necessária.
O xmloxide inclui alvos de fuzzing para testes de segurança:
# Instalar cargo-fuzz (requer nightly)
cargo install cargo-fuzz
# Executar um alvo 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
Versão mínima do Rust suportada: 1.81
PushParser) atualmente faz buffer de todos os dados enviados e realiza a análise completa no finish(), em vez de fazer streaming real como o xmlParseChunk do libxml2. O streaming SAX (parse_sax para XML, html5::sax::parse_html5_sax para HTML5) está disponível como alternativa para processamento de grandes documentos com restrição de memória.namespace:: — o eixo namespace:: retorna o nó do elemento quando os namespaces em âmbito coincidem (em vez de materializar nós de namespace separados), seguindo o mesmo padrão do eixo de atributos.Consulte CONTRIBUTING.md para configuração de desenvolvimento e diretrizes.
Consulte CHANGELOG.md para o histórico de versões.
MIT
| Módulo | Descrição |
|---|
tree | Árvore DOM baseada em arena (Document, NodeId, NodeKind) |
parser | Analisador descendente recursivo XML 1.0 com recuperação de erros |
parser::push | Analisador push/incremental para entrada fragmentada |
html | Analisador HTML 4.01 tolerante a erros |
html5 | Analisador HTML Living Standard WHATWG (tokenizer + construtor de árvore) |
html5::sax | API de streaming semelhante a SAX para HTML5 (sem construção de árvore DOM) |
css | Motor de seletores CSS para consulta de árvores de documentos |
sax | Analisador orientado por eventos de streaming SAX2 |
reader | API de análise pull baseada em XmlReader |
serial | Serializadores XML, HTML e HTML5, mais XML Canónico (C14N) |
xpath | Analisador de expressões e avaliador XPath 1.0+ |
validation::dtd | Análise e validação DTD |
validation::relaxng | Validação de esquemas RelaxNG |
validation::xsd | Validação XML Schema (XSD) |
validation::schematron | Validação baseada em regras ISO Schematron |
serde_xml | (De)serialização Serde XML (funcionalidade opcional serde) |
async_xml | Análise assíncrona via tokio::io::AsyncRead (funcionalidade opcional async) |
xinclude | Inclusão de documentos XInclude 1.0 |
catalog | Catálogos XML OASIS para resolução de URI |
encoding | Deteção e transcodificação de codificação de caracteres |
ffi | Ligações FFI C/C++ (include/xmloxide.h) |
| Documento | Tamanho | xmloxide | libxml2 | Resultado |
|---|
| Atom feed | 4.9 KB | 26.7 µs (176 MiB/s) | 25.5 µs (184 MiB/s) | ~4% mais lento |
| Desenho SVG | 6.3 KB | 58.5 µs (103 MiB/s) | 65.6 µs (92 MiB/s) | 12% mais rápido |
| POM Maven | 11.5 KB | 76.9 µs (142 MiB/s) | 74.2 µs (148 MiB/s) | ~4% mais lento |
| Página XHTML | 10.2 KB | 69.5 µs (139 MiB/s) | 61.5 µs (157 MiB/s) | ~13% mais lento |
| Grande (374 KB) | 374 KB | 2.15 ms (169 MiB/s) | 2.08 ms (175 MiB/s) | ~3% mais lento |
| Documento | Tamanho | xmloxide | libxml2 | Resultado |
|---|
| Atom feed | 4.9 KB | 11.3 µs | 17.5 µs | 1,5x mais rápido |
| POM Maven | 11.5 KB | 20.1 µs | 47.5 µs | 2,4x mais rápido |
| Grande (374 KB) | 374 KB | 614 µs | 1397 µs | 2,3x mais rápido |
| Expressão | xmloxide | libxml2 | Resultado |
|---|
Caminho simples (//entry/title) | 1.51 µs | 1.63 µs | 8% mais rápido |
Predicado de atributo (//book[@id]) | 5.91 µs | 15.99 µs | 2,7x mais rápido |
Função count() | 1.09 µs | 1.67 µs | 1,5x mais rápido |
Função string() | 1.32 µs | 1.77 µs | 1,3x mais 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álise HTML5) | html5::parse_html5 | — |
| (Fragmento HTML5 / innerHTML) | html5::parse_html5_with_options | — |
| (Streaming HTML5) | html5::sax::parse_html5_sax | — |
(Seletores CSS / querySelector) | css::select | — |
xmlFreeDoc | (descarte o 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 |
| (Validação 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 |