A ground-up, memory-safe Rust replacement for libxml2 — the XML library that underpins PHP, Python's lxml, Ruby's Nokogiri, PostgreSQL's XML functions, LibreOffice, and tens of thousands of other projects.
libxml2 is one of the most widely deployed C libraries in existence. It is also one of the most CVE-laden — hundreds of memory-safety vulnerabilities over 25 years, almost all structurally caused by manual memory management in C: use-after-free, heap overflows, integer overflows, null pointer dereferences. Rust's ownership model eliminates this entire bug class at compile time.
This project is a complete, drop-in replacement:
- Full feature parity with libxml2 2.x (SAX, DOM, XPath, XSD, DTD, RelaxNG, XInclude, C14N, HTML parser, and more)
- A C ABI compatibility layer — binary drop-in replacement via
LD_PRELOADor static linking - Secure-by-default configuration (XXE disabled, entity expansion limited out of the box)
- W3C/OASIS conformance suite compliance at or above libxml2's level
Phase 1 is complete. Phase 2 is in active development. We are actively seeking contributors, reviewers, and domain experts.
See docs/architecture/overview.md for the full technical design.
| Phase | Scope | Status |
|---|---|---|
| 1 — Core Parser | XML 1.0 tokenizer, namespace resolution, arena DOM, entity decoding, encoding transcoding, serialization | ✅ Complete |
| 2 — Extended | XPath 1.0, DTD validation, Reader/Writer, HTML5, C ABI | 🔧 In Progress |
| 3 — Validation | XSD, RelaxNG, XInclude, C14N, Catalogs | 🔲 Planned |
| 4 — XSLT + Maturity | XSLT 1.0, Schematron, full C ABI parity, security audit | 🔲 Planned |
| Crate | What works |
|---|---|
xml-chars |
Unicode XML 1.0 §2.2 character class tables |
xml-tokenizer |
Zero-copy byte scanner; emits SAX2-style tokens; UTF-8 BOM stripping |
xml-ns |
NsResolver scope stack; prefix→URI lookup; QName resolution |
xml-tree |
Arena DOM (NodeId index tree); Builder from token stream; namespace-aware elements and attributes; entity/character-reference decoding (&, &#xNN;, …); to_xml_string / serialize_node / write_xml serialization |
libxml2-rs |
parse_bytes / parse_str / parse_file / parse_reader; ParserOptions with secure defaults; encoding detection and transcoding (UTF-16 LE/BE BOM, XML declaration sniff, encoding_rs); Document::to_xml_string / to_xml_string_formatted / write_xml |
- Arena + index tree —
NodeId(u32)indices into a flatVec<NodeData>. NoRc<RefCell<>>, no pointer chasing.Document: Send + Sync. - Zero-copy tokenizer — borrows from input, SIMD byte scanning via
memchr. Only allocates on entity expansion. - XPath bytecode VM — compiles XPath expressions to an instruction sequence rather than
Box<dyn Expression>vtable dispatch. encoding_rsfor all character encoding transcoding (the same library used in Firefox).- Secure defaults —
ParserOptions::default()disables external entities, limits entity expansion depth/size, and limits nesting depth. Opt-in to libxml2-compatible permissive mode for compatibility testing.
crates/
├── xml-chars/ # Unicode XML character class tables (no_std)
├── xml-tokenizer/ # Zero-copy byte scanner (no_std + alloc)
├── xml-ns/ # Namespace URI/prefix types
├── xml-tree/ # Arena-based mutable DOM
├── xml-xpath/ # XPath 1.0 evaluator
├── xml-schema/ # W3C XML Schema (XSD) validation
├── xml-relaxng/ # RelaxNG validation
├── xml-xinclude/ # XInclude processing
├── xml-c14n/ # Canonical XML
├── xml-catalog/ # OASIS XML Catalogs
├── html-parser/ # HTML5 parser (html5ever integration)
├── libxml2-rs/ # Unified public API facade
└── libxml2-rs-c/ # C ABI compatibility layer (cdylib)
tools/
└── xmllint-rs/ # xmllint-compatible CLI tool
The library is not yet published to crates.io. Add it as a path dependency from a workspace checkout.
# Cargo.toml (path dep until crates.io publish)
[dependencies]
libxml2-rs = { path = "path/to/libxml2-rs/crates/libxml2-rs" }use libxml2_rs::{parse_str, ParserOptions};
let doc = parse_str(
r#"<root><child id="1">hello</child></root>"#,
&ParserOptions::default(),
)?;
let tree = doc.tree();
let root = tree.first_child(doc.root()).unwrap();
println!("Root: {}", tree.name(root)); // "root"
for child in tree.children(root) {
let text = tree.first_child(child)
.map(|t| tree.value(t))
.unwrap_or("");
println!(" {}: {}", tree.name(child), text); // " child: hello"
}
// Serialize back to XML
println!("{}", doc.to_xml_string());
// → <root><child id="1">hello</child></root>ParserOptions::default() is safe out of the box — XXE is off, entity expansion is capped, and nesting depth is limited. Use ParserOptions::libxml2_compat() only in tests that need full compatibility with the reference implementation.
We welcome contributions at every level — from reviewing the architecture to implementing individual crates, writing tests, or improving documentation.
Please read CONTRIBUTING.md before opening a pull request.
Areas where help is especially valuable right now:
- Review of docs/architecture/overview.md — feedback from XML spec experts, parser engineers, and libxml2 users is invaluable
- XSD 1.0 (XML Schema) — the largest and most complex module; domain expertise welcome
- W3C/OASIS conformance test harness infrastructure
- XSLT 1.0 implementation design
Security is the primary motivation for this project. If you discover a vulnerability, please follow the responsible disclosure process in SECURITY.md.
All parser inputs are treated as untrusted by default. We participate in OSS-Fuzz for continuous fuzzing at scale.
Apache License 2.0 — see LICENSE.
Compatible with libxml2's MIT license. See CONTRIBUTING.md for the DCO sign-off requirement.