
Una reimplementazione in puro Rust di libxml2
# xmloxide
[](https://github.com/jonwiggins/xmloxide/actions/workflows/ci.yml)
[](https://crates.io/crates/xmloxide)
[](https://docs.rs/xmloxide)
[](LICENSE)
[](https://www.rust-lang.org)
Una reimplementazione in Rust puro di [libxml2](https://gitlab.gnome.org/GNOME/libxml2) — la libreria di parsing XML/HTML de facto standard nel mondo open-source.
libxml2 è diventata ufficialmente non mantenuta a dicembre 2025 con problemi di sicurezza noti. xmloxide punta ad essere un sostituto memory-safe e ad alte prestazioni che supera le stesse suite di test di conformità.
## Caratteristiche
- **Memory-safe** — albero basato su arena con zero `unsafe` nell'API pubblica
- **Conforme** — 100% di superamento della W3C XML Conformance Test Suite (1727/1727 test applicabili)
- **Recupero errori** — analizza XML malformato e produce comunque un albero utilizzabile, proprio come libxml2
- **Multiple API di parsing** — DOM tree, SAX2 streaming, XmlReader pull, push/incrementale
- **Parser HTML** — parsing HTML 4.01 tollerante agli errori con chiusura automatica ed elementi void
- **Parser WHATWG HTML5** — tokenizer e tree builder completi dell'[HTML Living Standard](https://html.spec.whatwg.org/) (8810/8810 html5lib-tests superati)
- **Streaming HTML5** — API callback simile a SAX per HTML5 (`html5::sax`) che avvolge il tokenizer senza costruire un albero DOM
- **Selettori CSS** — interroga elementi con sintassi CSS familiare (`css::select`) inclusi combinatori, pseudo-classi e ricerca rapida per `#id`
- **XPath 1.0+** — parser di espressioni ed evaluatore completo con tutte le funzioni principali di XPath 1.0 più funzioni chiave di XPath 2.0 (`matches()`, `replace()`, `tokenize()`, `upper-case()`, `lower-case()`, `abs()`, `min()`, `max()`, e altro)
- **Validazione** — DTD, RelaxNG, XML Schema (XSD) e ISO Schematron (ISO/IEC 19757-3)
- **Integrazione Serde** — funzionalità opzionale `serde` per (de)serializzazione XML da/verso tipi Rust
- **Parsing asincrono** — funzionalità opzionale `async` per il parsing da sorgenti `tokio::io::AsyncRead`
- **XML Canonico** — serializzazione C14N 1.0 e Exclusive C14N
- **XInclude** — elaborazione di inclusione di documenti
- **Cataloghi XML** — Cataloghi XML OASIS per risoluzione URI
- **CLI `xmllint`** — strumento a riga di comando per analizzare, validare e interrogare XML
- **Zero-copy dove possibile** — string interning per confronti rapidi
- **Nessuno stato globale** — ogni `Document` è autonomo e `Send + Sync`
- **FFI C/C++** — API C completa con file header (`include/xmloxide.h`) per l'incorporamento in progetti C/C++
- **Dipendenze minime** — solo `encoding_rs` (la libreria ha zero altre dipendenze; `clap` è solo per la CLI)
## Avvio rapido
```rust
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");
```
### Serializzazione
```rust
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>");
```
### Query XPath
```rust
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
```rust
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();
```
### Parsing HTML
```rust
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"));
```
### Selettori CSS
```rust
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");
```
### Parsing HTML5 (WHATWG)
```rust
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"));
```
Anche il parsing di frammenti (l'algoritmo alla base di `innerHTML`) è supportato:
```rust
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 (simile a SAX)
```rust
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="https://github.com/jonwiggins/xmloxide/blob/main/page">Link</a>"#, &mut handler);
assert_eq!(handler.hrefs, vec!["/page"]);
```
### Recupero errori
```rust
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);
}
```
## Strumento CLI
```sh
# Analizza e stampa in modo leggibile
xmllint --format document.xml
# Valida rispetto a uno schema
xmllint --schema schema.xsd document.xml
xmllint --relaxng schema.rng document.xml
xmllint --schematron schema.sch document.xml
xmllint --dtdvalid schema.dtd document.xml
# Query XPath
xmllint --xpath "//title" document.xml
# XML canonico
xmllint --c14n document.xml
# Parsing HTML
xmllint --html page.html
```
## Panoramica dei moduli
| Modulo | Descrizione |
|--------|-------------|
| `tree` | Albero DOM basato su arena (`Document`, `NodeId`, `NodeKind`) |
| `parser` | Parser discendente ricorsivo XML 1.0 con recupero errori |
| `parser::push` | Parser push/incrementale per input a blocchi |
| `html` | Parser HTML 4.01 tollerante agli errori |
| `html5` | Parser WHATWG HTML Living Standard (tokenizer + tree builder) |
| `html5::sax` | API streaming simile a SAX per HTML5 (nessun albero DOM costruito) |
| `css` | Motore di selettori CSS per interrogare alberi di documenti |
| `sax` | Parser SAX2 streaming guidato da eventi |
| `reader` | API di parsing basata su pull XmlReader |
| `serial` | Serializzatori XML, HTML e HTML5, più Canonical XML (C14N) |
| `xpath` | Parser di espressioni ed evaluatore XPath 1.0+ |
| `validation::dtd` | Parsing e validazione DTD |
| `validation::relaxng` | Validazione schema RelaxNG |
| `validation::xsd` | Validazione XML Schema (XSD) |
| `validation::schematron` | Validazione basata su regole ISO Schematron |
| `serde_xml` | (De)serializzazione Serde XML (funzionalità `serde` opzionale) |
| `async_xml` | Parsing asincrono tramite `tokio::io::AsyncRead` (funzionalità `async` opzionale) |
| `xinclude` | Inclusione di documenti XInclude 1.0 |
| `catalog` | Cataloghi XML OASIS per risoluzione URI |
| `encoding` | Rilevamento e transcodifica della codifica dei caratteri |
| `ffi` | Binding FFI C/C++ (`include/xmloxide.h`) |
## Prestazioni
Il throughput di parsing è competitivo con libxml2 — entro 3-4% sulla maggior parte dei documenti e **12% più veloce** su SVG. La serializzazione è **1.5-2.4x più veloce** grazie al design dell'albero basato su arena. XPath è **1.1-2.7x più veloce** in tutti i benchmark.
**Parsing:**
| Documento | Dimensione | xmloxide | libxml2 | Risultato |
|-----------|-----------|----------|---------|-----------|
| Feed Atom | 4.9 KB | 26.7 µs (176 MiB/s) | 25.5 µs (184 MiB/s) | ~4% più lento |
| Disegno SVG | 6.3 KB | 58.5 µs (103 MiB/s) | 65.6 µs (92 MiB/s) | **12% più veloce** |
| Maven POM | 11.5 KB | 76.9 µs (142 MiB/s) | 74.2 µs (148 MiB/s) | ~4% più lento |
| Pagina XHTML | 10.2 KB | 69.5 µs (139 MiB/s) | 61.5 µs (157 MiB/s) | ~13% più lento |
| Grande (374 KB) | 374 KB | 2.15 ms (169 MiB/s) | 2.08 ms (175 MiB/s) | ~3% più lento |
**Serializzazione:**
| Documento | Dimensione | xmloxide | libxml2 | Risultato |
|-----------|-----------|----------|---------|-----------|
| Feed Atom | 4.9 KB | 11.3 µs | 17.5 µs | **1.5x più veloce** |
| Maven POM | 11.5 KB | 20.1 µs | 47.5 µs | **2.4x più veloce** |
| Grande (374 KB) | 374 KB | 614 µs | 1397 µs | **2.3x più veloce** |
**XPath:**
| Espressione | xmloxide | libxml2 | Risultato |
|-------------|----------|---------|-----------|
| Percorso semplice (`//entry/title`) | 1.51 µs | 1.63 µs | **8% più veloce** |
| Predicato su attributo (`//book[@id]`) | 5.91 µs | 15.99 µs | **2.7x più veloce** |
| Funzione `count()` | 1.09 µs | 1.67 µs | **1.5x più veloce** |
| Funzione `string()` | 1.32 µs | 1.77 µs | **1.3x più veloce** |
Ottimizzazioni chiave: albero basato su arena per serializzazione rapida, pre-controlli a livello di byte per la validazione dei caratteri, scansione bulk del testo, percorsi veloci ASCII per il parsing dei nomi, suddivisione zero-copy dei nomi degli elementi, risoluzione inline delle entità, fusione del passo XPath `//` con espansione degli assi fusi, accessori inline dell'albero e percorsi veloci per il test dei nomi per gli assi figlio/discendente.
```sh
# Esegui benchmark (richiede la libreria di sistema libxml2)
cargo bench --features bench-libxml2 --bench comparison_bench
```
## Test
- **1078 test unitari** in tutti i moduli
- **138 test FFI** che coprono l'intera superficie dell'API C (inclusi SAX, Schematron e CSS)
- **Suite di compatibilità libxml2** — 119/119 test superati (100%) che coprono parsing XML, namespace, rilevamento errori e parsing HTML
- **W3C XML Conformance Test Suite** — 1727/1727 test applicabili superati (100%)
- **html5lib-tests** — 7032/7032 test del tokenizer + 1778/1778 test di costruzione dell'albero (100%)
- **Test di integrazione** che coprono documenti XML/HTML reali, casi limite e recupero errori
```sh
cargo test --all-features
```
## FFI C/C++
xmloxide fornisce un'API compatibile con C per l'incorporamento in progetti C/C++ (come Chromium, motori di gioco o qualsiasi base di codice che utilizza attualmente libxml2).
```sh
# Compila librerie condivise + statiche (usa il Makefile incluso)
make
# Oppure compila singolarmente:
make shared # .so / .dylib / .dll
make static # .a / .lib
# Compila ed esegui l'esempio C
make example
```
```c
#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 completa — inclusi navigazione e mutazione dell'albero, valutazione XPath, serializzazione (normale e formattata), parsing HTML/HTML5, validazione DTD/RelaxNG/XSD/Schematron, C14N, streaming SAX, XmlReader, parser push e Cataloghi XML — è dichiarata in [`include/xmloxide.h`](https://github.com/jonwiggins/xmloxide/blob/main/include/xmloxide.h).
## Migrazione da 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` |
| (Parsing HTML5) | `html5::parse_html5` | — |
| (Frammento HTML5 / innerHTML) | `html5::parse_html5_with_options` | — |
| (Streaming HTML5) | `html5::sax::parse_html5_sax` | — |
| (Selettori CSS / `querySelector`) | `css::select` | — |
| `xmlFreeDoc` | (rilascia `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` |
| (Validazione Schematron) | `validation::schematron::validate_schematron` | `xmloxide_validate_schematron` |
| `xmlXIncludeProcess` | `xinclude::process_xincludes` | `xmloxide_process_xincludes` |
| `xmlLoadCatalog` | `Catalog::parse` | `xmloxide_parse_catalog` |
| Callback `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` |
**Sicurezza dei thread:** A differenza di libxml2, xmloxide non ha stato globale. Ogni `Document` è autonomo e `Send + Sync`. Il livello FFI utilizza storage locale del thread per l'ultimo messaggio di errore — ogni thread ha il proprio stato di errore. Non sono necessarie funzioni di inizializzazione o pulizia.
## Fuzzing
xmloxide include target di fuzzing per test di sicurezza:
```sh
# Installa cargo-fuzz (richiede nightly)
cargo install cargo-fuzz
# Esegui un target di 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
```
## Compilazione
```sh
cargo build
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo bench
```
Versione minima supportata di Rust: **1.81**
## Limitazioni
- **Nessun XML 1.1** — xmloxide implementa solo XML 1.0 (Quinta Edizione). XML 1.1 è raramente utilizzato e non previsto.
- **Nessun XSLT** — XSLT è una specifica separata (libxslt) ed è fuori dall'ambito.
- **Parser HTML** — vengono forniti sia un parser HTML 4.01 (che corrisponde al comportamento di libxml2) sia un parser WHATWG HTML5 completo. Il parser HTML5 supera il 100% degli html5lib-tests.
- **Il parser push bufferizza internamente** — l'API del parser push/incrementale (`PushParser`) attualmente bufferizza tutti i dati inviati ed esegue il parsing completo su `finish()`, anziché uno streaming reale come `xmlParseChunk` di libxml2. Lo streaming SAX (`parse_sax` per XML, `html5::sax::parse_html5_sax` per HTML5) è disponibile come alternativa per l'elaborazione di documenti di grandi dimensioni con memoria limitata.
- **Asse XPath `namespace::`** — l'asse `namespace::` restituisce il nodo elemento quando i namespace in scope corrispondono (anziché materializzare nodi namespace separati), seguendo lo stesso schema dell'asse degli attributi.
## Contribuire
Vedere [CONTRIBUTING.md](https://github.com/jonwiggins/xmloxide/blob/main/CONTRIBUTING.md) per configurazione di sviluppo e linee guida.
## Changelog
Vedere [CHANGELOG.md](https://github.com/jonwiggins/xmloxide/blob/main/CHANGELOG.md) per la cronologia delle versioni.
## Licenza
MIT