Back to updates
New releaseAug 9, 2026

xmloxide v0.5.0

A pure Rust reimplementation of libxml2

Share

xmloxide

CI crates.io docs.rs License: MIT MSRV

A pure Rust reimplementation of libxml2 — the de facto standard XML/HTML parsing library in the open-source world.

libxml2 became officially unmaintained in December 2025 with known security issues. xmloxide aims to be a memory-safe, high-performance replacement that passes the same conformance test suites.

Features

  • Memory-safe — arena-based tree with zero unsafe in the public API
  • Conformant — 100% pass rate on the W3C XML Conformance Test Suite (1727/1727 applicable tests)
  • Error recovery — parse malformed XML and still produce a usable tree, just like libxml2
  • Multiple parsing APIs — DOM tree, SAX2 streaming, XmlReader pull, push/incremental
  • HTML parser — error-tolerant HTML 4.01 parsing with auto-closing and void elements
  • WHATWG HTML5 parser — full HTML Living Standard tokenizer and tree builder (8810/8810 html5lib-tests passing)
  • HTML5 streaming — SAX-like callback API for HTML5 (html5::sax) that wraps the tokenizer without building a DOM tree
  • CSS selectors — query elements with familiar CSS syntax (css::select) including combinators, pseudo-classes, and fast #id lookup
  • XPath 1.0+ — full expression parser and evaluator with all XPath 1.0 core functions plus key XPath 2.0 functions (matches(), replace(), tokenize(), upper-case(), lower-case(), abs(), min(), max(), and more)
  • Validation — DTD, RelaxNG, XML Schema (XSD), and ISO Schematron (ISO/IEC 19757-3) validation
  • Serde integration — optional serde feature for XML (de)serialization to/from Rust types
  • Async parsing — optional async feature for parsing from tokio::io::AsyncRead sources
  • Canonical XML — C14N 1.0 and Exclusive C14N serialization
  • XInclude — document inclusion processing
  • XML Catalogs — OASIS XML Catalogs for URI resolution
  • xmllint CLI — command-line tool for parsing, validating, and querying XML
  • Zero-copy where possible — string interning for fast comparisons
  • No global state — each Document is self-contained and Send + Sync
  • C/C++ FFI — full C API with header file (include/xmloxide.h) for embedding in C/C++ projects
  • Minimal dependencies — only encoding_rs (library has zero other deps; clap is CLI-only)

Quick Start

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");

Serialization

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>");

XPath Queries

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();

HTML Parsing

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"));

CSS Selectors

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");

HTML5 Parsing (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"));

Fragment parsing (the algorithm behind innerHTML) is also supported:

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();

HTML5 Streaming (SAX-like)

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"]);

Error Recovery

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);
}

CLI Tool

# Parse and pretty-print
xmllint --format document.xml

# Validate against a 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

# XPath query
xmllint --xpath "//title" document.xml

# Canonical XML
xmllint --c14n document.xml

# Parse HTML
xmllint --html page.html

Module Overview

ModuleDescription
treeArena-based DOM tree (Document, NodeId, NodeKind)
parserXML 1.0 recursive descent parser with error recovery
parser::pushPush/incremental parser for chunked input
htmlError-tolerant HTML 4.01 parser
html5WHATWG HTML Living Standard parser (tokenizer + tree builder)
html5::saxStreaming SAX-like API for HTML5 (no DOM tree built)
cssCSS selector engine for querying document trees
saxSAX2 streaming event-driven parser
readerXmlReader pull-based parsing API
serialXML, HTML, and HTML5 serializers, plus Canonical XML (C14N)
xpathXPath 1.0+ expression parser and evaluator
validation::dtdDTD parsing and validation
validation::relaxngRelaxNG schema validation
validation::xsdXML Schema (XSD) validation
validation::schematronISO Schematron rule-based validation
serde_xmlSerde XML (de)serialization (optional serde feature)
async_xmlAsync parsing via tokio::io::AsyncRead (optional async feature)
xincludeXInclude 1.0 document inclusion
catalogOASIS XML Catalogs for URI resolution
encodingCharacter encoding detection and transcoding
ffiC/C++ FFI bindings (include/xmloxide.h)

Performance

Parsing throughput is competitive with libxml2 — within 3-4% on most documents, and 12% faster on SVG. Serialization is 1.5-2.4x faster thanks to the arena-based tree design. XPath is 1.1-2.7x faster across all benchmarks.

Parsing:

DocumentSizexmloxidelibxml2Result
Atom feed4.9 KB26.7 µs (176 MiB/s)25.5 µs (184 MiB/s)~4% slower
SVG drawing6.3 KB58.5 µs (103 MiB/s)65.6 µs (92 MiB/s)12% faster
Maven POM11.5 KB76.9 µs (142 MiB/s)74.2 µs (148 MiB/s)~4% slower
XHTML page10.2 KB69.5 µs (139 MiB/s)61.5 µs (157 MiB/s)~13% slower
Large (374 KB)374 KB2.15 ms (169 MiB/s)2.08 ms (175 MiB/s)~3% slower

Serialization:

DocumentSizexmloxidelibxml2Result
Atom feed4.9 KB11.3 µs17.5 µs1.5x faster
Maven POM11.5 KB20.1 µs47.5 µs2.4x faster
Large (374 KB)374 KB614 µs1397 µs2.3x faster

XPath:

Expressionxmloxidelibxml2Result
Simple path (//entry/title)1.51 µs1.63 µs8% faster
Attribute predicate (//book[@id])5.91 µs15.99 µs2.7x faster
count() function1.09 µs1.67 µs1.5x faster
string() function1.32 µs1.77 µs1.3x faster

Key optimizations: arena-based tree for fast serialization, byte-level pre-checks for character validation, bulk text scanning, ASCII fast paths for name parsing, zero-copy element name splitting, inline entity resolution, XPath // step fusion with fused axis expansion, inlined tree accessors, and name-test fast paths for child/descendant axes.

# Run benchmarks (requires libxml2 system library)
cargo bench --features bench-libxml2 --bench comparison_bench

Testing

  • 1078 unit tests across all modules
  • 138 FFI tests covering the full C API surface (including SAX, Schematron, and CSS)
  • libxml2 compatibility suite — 119/119 tests passing (100%) covering XML parsing, namespaces, error detection, and HTML parsing
  • W3C XML Conformance Test Suite — 1727/1727 applicable tests passing (100%)
  • html5lib-tests — 7032/7032 tokenizer tests + 1778/1778 tree construction tests (100%)
  • Integration tests covering real-world XML/HTML documents, edge cases, and error recovery
cargo test --all-features

C/C++ FFI

xmloxide provides a C-compatible API for embedding in C/C++ projects (like Chromium, game engines, or any codebase that currently uses libxml2).

# Build shared + static libraries (uses the included Makefile)
make

# Or build individually:
make shared   # .so / .dylib / .dll
make static   # .a / .lib

# Build and run the C example
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);

The full API — including tree navigation and mutation, XPath evaluation, serialization (plain and pretty-printed), HTML/HTML5 parsing, DTD/RelaxNG/XSD/Schematron validation, C14N, SAX streaming, XmlReader, push parser, and XML Catalogs — is declared in include/xmloxide.h.

Migrating from libxml2

libxml2xmloxide (Rust)xmloxide (C FFI)
xmlReadMemoryDocument::parse_strxmloxide_parse_str
xmlReadFileDocument::parse_filexmloxide_parse_file
xmlParseDocDocument::parse_bytesxmloxide_parse_bytes
htmlReadMemoryhtml::parse_htmlxmloxide_parse_html
(HTML5 parsing)html5::parse_html5
(HTML5 fragment / innerHTML)html5::parse_html5_with_options
(HTML5 streaming)html5::sax::parse_html5_sax
(CSS selectors / querySelector)css::select
xmlFreeDoc(drop Document)xmloxide_free_doc
xmlDocGetRootElementdoc.root_element()xmloxide_doc_root_element
xmlNodeGetContentdoc.text_content(id)xmloxide_node_text_content
xmlNodeSetContentdoc.set_text_content(id, s)xmloxide_set_text_content
xmlGetPropdoc.attribute(id, name)xmloxide_node_attribute
xmlSetPropdoc.set_attribute(...)xmloxide_set_attribute
xmlNewNodedoc.create_node(...)xmloxide_create_element
xmlNewTextdoc.create_node(Text{..})xmloxide_create_text
xmlAddChilddoc.append_child(p, c)xmloxide_append_child
xmlAddPrevSiblingdoc.insert_before(ref, c)xmloxide_insert_before
xmlUnlinkNodedoc.remove_node(id)xmloxide_remove_node
xmlCopyNodedoc.clone_node(id, deep)xmloxide_clone_node
xmlGetIDdoc.element_by_id(s)xmloxide_element_by_id
xmlDocDumpMemoryserial::serialize(&doc)xmloxide_serialize
xmlDocDumpFormatMemoryserial::serialize_with_optionsxmloxide_serialize_pretty
htmlDocDumpMemoryserial::html::serialize_htmlxmloxide_serialize_html
xmlC14NDocDumpMemoryserial::c14n::canonicalizexmloxide_canonicalize
xmlXPathEvalExpressionxpath::evaluatexmloxide_xpath_eval
xmlValidateDtdvalidation::dtd::validatexmloxide_validate_dtd
xmlRelaxNGValidateDocvalidation::relaxng::validatexmloxide_validate_relaxng
xmlSchemaValidateDocvalidation::xsd::validate_xsdxmloxide_validate_xsd
(Schematron validation)validation::schematron::validate_schematronxmloxide_validate_schematron
xmlXIncludeProcessxinclude::process_xincludesxmloxide_process_xincludes
xmlLoadCatalogCatalog::parsexmloxide_parse_catalog
xmlSAX2... callbackssax::SaxHandler traitxmloxide_sax_parse
xmlTextReaderReadreader::XmlReaderxmloxide_reader_read
xmlCreatePushParserCtxtparser::PushParserxmloxide_push_parser_new
xmlParseChunkPushParser::pushxmloxide_push_parser_push

Thread safety: Unlike libxml2, xmloxide has no global state. Each Document is self-contained and Send + Sync. The FFI layer uses thread-local storage for the last error message — each thread has its own error state. No initialization or cleanup functions are needed.

Fuzzing

xmloxide includes fuzz targets for security testing:

# Install cargo-fuzz (requires nightly)
cargo install cargo-fuzz

# Run a fuzz target
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

Building

cargo build
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo bench

Minimum supported Rust version: 1.81

Limitations

  • No XML 1.1 — xmloxide implements XML 1.0 (Fifth Edition) only. XML 1.1 is rarely used and not planned.
  • No XSLT — XSLT is a separate specification (libxslt) and is out of scope.
  • HTML parsers — both an HTML 4.01 parser (matching libxml2's behavior) and a full WHATWG HTML5 parser are provided. The HTML5 parser passes 100% of html5lib-tests.
  • Push parser buffers internally — the push/incremental parser API (PushParser) currently buffers all pushed data and performs the full parse on finish(), rather than truly streaming like libxml2's xmlParseChunk. SAX streaming (parse_sax for XML, html5::sax::parse_html5_sax for HTML5) is available as an alternative for memory-constrained large-document processing.
  • XPath namespace:: axis — the namespace:: axis returns the element node when in-scope namespaces match (rather than materializing separate namespace nodes), following the same pattern as the attribute axis.

Contributing

See CONTRIBUTING.md for development setup and guidelines.

Changelog

See CHANGELOG.md for version history.

License

MIT

Categories