
xmloxide v0.5.0
Чистая реализация libxml2 на Rust
xmloxide
Чистая реализация libxml2 на Rust — де-факто стандартной библиотеки для разбора XML/HTML в мире открытого ПО.
libxml2 перестала официально поддерживаться в декабре 2025 года с известными проблемами безопасности. xmloxide призван стать безопасной по памяти высокопроизводительной заменой, проходящей те же наборы тестов на соответствие.
Возможности
- Безопасность памяти — аренное дерево с нулевым
unsafeв публичном API - Соответствие стандартам — 100% прохождение тестового набора W3C XML Conformance Test Suite (1727/1727 применимых тестов)
- Восстановление после ошибок — разбор некорректного XML с получением используемого дерева, как в libxml2
- Несколько API разбора — DOM-дерево, потоковый SAX2, pull-парсер XmlReader, инкрементальный разбор
- HTML-парсер — устойчивый к ошибкам парсер HTML 4.01 с авто-закрытием и void-элементами
- HTML5-парсер WHATWG — полный токенизатор и построитель дерева по HTML Living Standard (8810/8810 пройденных html5lib-tests)
- Потоковый HTML5 — SAX-подобный API обратных вызовов для HTML5 (
html5::sax), оборачивающий токенизатор без построения DOM-дерева - CSS-селекторы — запросы элементов с привычным CSS-синтаксисом (
css::select) включая комбинаторы, псевдоклассы и быстрый поиск по#id - XPath 1.0+ — полный парсер и вычислитель выражений со всеми базовыми функциями XPath 1.0 и ключевыми функциями XPath 2.0 (
matches(),replace(),tokenize(),upper-case(),lower-case(),abs(),min(),max()и другими) - Валидация — DTD, RelaxNG, XML Schema (XSD) и ISO Schematron (ISO/IEC 19757-3)
- Интеграция с Serde — опциональная возможность
serdeдля (де)сериализации XML в/из типов Rust - Асинхронный разбор — опциональная возможность
asyncдля разбора изtokio::io::AsyncRead - Канонический XML — C14N 1.0 и Exclusive C14N сериализация
- XInclude — обработка включения документов
- XML-каталоги — OASIS XML Catalogs для разрешения URI
- CLI-инструмент
xmllint— командная утилита для разбора, валидации и запросов XML - Zero-copy где возможно — интернирование строк для быстрых сравнений
- Отсутствие глобального состояния — каждый
Documentсамодостаточен иSend + Sync - C/C++ FFI — полный C-API с заголовочным файлом (
include/xmloxide.h) для встраивания в проекты на C/C++ - Минимальные зависимости — только
encoding_rs(библиотека не имеет других зависимостей;clapиспользуется только в 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>");
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
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
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-селекторы
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 (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"));
Также поддерживается разбор фрагментов (алгоритм для innerHTML):
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 (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"]);
Восстановление после ошибок
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-инструмент
# Разбор и красивый вывод
xmllint --format document.xml
# Валидация по схеме
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-запрос
xmllint --xpath "//title" document.xml
# Канонический XML
xmllint --c14n document.xml
# Разбор HTML
xmllint --html page.html
Обзор модулей
| Модуль | Описание |
|---|---|
tree | Аренное DOM-дерево (Document, NodeId, NodeKind) |
parser | Рекурсивный нисходящий парсер XML 1.0 с восстановлением после ошибок |
parser::push | Push/инкрементальный парсер для входных данных по частям |
html | Устойчивый к ошибкам парсер HTML 4.01 |
html5 | Парсер WHATWG HTML Living Standard (токенизатор + построитель дерева) |
html5::sax | Потоковый SAX-подобный API для HTML5 (без построения DOM-дерева) |
css | Движок CSS-селекторов для запросов к деревьям документов |
sax | Событийный потоковый парсер SAX2 |
reader | Pull-парсер XmlReader |
serial | Сериализаторы XML, HTML и HTML5, а также канонический XML (C14N) |
xpath | Парсер и вычислитель выражений XPath 1.0+ |
validation::dtd | Разбор и валидация DTD |
validation::relaxng | Валидация схем RelaxNG |
validation::xsd | Валидация XML-схем (XSD) |
validation::schematron | Основанная на правилах валидация ISO Schematron |
serde_xml | (Де)сериализация Serde XML (опциональная возможность serde) |
async_xml | Асинхронный разбор через tokio::io::AsyncRead (опциональная возможность async) |
xinclude | Включение документов XInclude 1.0 |
catalog | OASIS XML Catalogs для разрешения URI |
encoding | Обнаружение и перекодировка кодировок символов |
ffi | Привязки C/C++ FFI (include/xmloxide.h) |
Производительность
Пропускная способность разбора конкурентоспособна с libxml2 — в пределах 3-4% на большинстве документов, а на SVG на 12% быстрее. Сериализация работает в 1.5-2.4 раза быстрее благодаря конструкции аренного дерева. XPath работает в 1.1-2.7 раза быстрее по всем бенчмаркам.
Разбор:
| Документ | Размер | xmloxide | libxml2 | Результат |
|---|---|---|---|---|
| Atom feed | 4.9 KB | 26.7 µs (176 MiB/s) | 25.5 µs (184 MiB/s) | ~4% медленнее |
| SVG drawing | 6.3 KB | 58.5 µs (103 MiB/s) | 65.6 µs (92 MiB/s) | на 12% быстрее |
| Maven POM | 11.5 KB | 76.9 µs (142 MiB/s) | 74.2 µs (148 MiB/s) | ~4% медленнее |
| XHTML page | 10.2 KB | 69.5 µs (139 MiB/s) | 61.5 µs (157 MiB/s) | ~13% медленнее |
| Большой (374 KB) | 374 KB | 2.15 ms (169 MiB/s) | 2.08 ms (175 MiB/s) | ~3% медленнее |
Сериализация:
| Документ | Размер | xmloxide | libxml2 | Результат |
|---|---|---|---|---|
| Atom feed | 4.9 KB | 11.3 µs | 17.5 µs | в 1.5x быстрее |
| Maven POM | 11.5 KB | 20.1 µs | 47.5 µs | в 2.4x быстрее |
| Большой (374 KB) | 374 KB | 614 µs | 1397 µs | в 2.3x быстрее |
XPath:
| Выражение | xmloxide | libxml2 | Результат |
|---|---|---|---|
Простой путь (//entry/title) | 1.51 µs | 1.63 µs | на 8% быстрее |
Предикат атрибута (//book[@id]) | 5.91 µs | 15.99 µs | в 2.7x быстрее |
Функция count() | 1.09 µs | 1.67 µs | в 1.5x быстрее |
Функция string() | 1.32 µs | 1.77 µs | в 1.3x быстрее |
Ключевые оптимизации: аренное дерево для быстрой сериализации, побайтовые предпроверки для валидации символов, пакетное сканирование текста, быстрые пути ASCII для разбора имён, zero-copy разбиение имён элементов, встраивание разрешения сущностей, слияние шагов // XPath с расширением осей, встраивание аксессоров дерева и быстрые пути проверки имён для осей потомков/потомков.
# Запуск бенчмарков (требуется системная библиотека libxml2)
cargo bench --features bench-libxml2 --bench comparison_bench
Тестирование
- 1078 модульных тестов по всем модулям
- 138 тестов FFI, покрывающих полный C-API (включая SAX, Schematron и CSS)
- Набор совместимости с libxml2 — 119/119 тестов пройдены (100%), охватывающих разбор XML, пространства имён, обнаружение ошибок и разбор HTML
- W3C XML Conformance Test Suite — 1727/1727 применимых тестов пройдены (100%)
- html5lib-tests — 7032/7032 тестов токенизатора + 1778/1778 тестов построения дерева (100%)
- Интеграционные тесты с реальными XML/HTML-документами, граничными случаями и восстановлением после ошибок
cargo test --all-features
C/C++ FFI
xmloxide предоставляет C-совместимый API для встраивания в проекты на C/C++ (например, Chromium, игровые движки или любые кодовые базы, использующие libxml2).
# Сборка разделяемой + статической библиотек (используется прилагаемый Makefile)
make
# Или сборка по отдельности:
make shared # .so / .dylib / .dll
make static # .a / .lib
# Сборка и запуск примера на 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);
Полный API — включая навигацию и изменение дерева, вычисление XPath, сериализацию (обычную и с форматированием), разбор HTML/HTML5, валидацию DTD/RelaxNG/XSD/Schematron, C14N, потоковый SAX, XmlReader, push-парсер и XML-каталоги — объявлен в include/xmloxide.h.
Миграция с 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 |
| (разбор HTML5) | html5::parse_html5 | — |
| (фрагмент HTML5 / innerHTML) | html5::parse_html5_with_options | — |
| (потоковый HTML5) | html5::sax::parse_html5_sax | — |
(CSS-селекторы / querySelector) | css::select | — |
xmlFreeDoc | (удалить 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 |
| (валидация Schematron) | validation::schematron::validate_schematron | xmloxide_validate_schematron |
xmlXIncludeProcess | xinclude::process_xincludes | xmloxide_process_xincludes |
xmlLoadCatalog | Catalog::parse | xmloxide_parse_catalog |
обратные вызовы xmlSAX2... | трейт 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 |
Потокобезопасность: В отличие от libxml2, xmloxide не имеет глобального состояния. Каждый Document самодостаточен и Send + Sync. Уровень FFI использует локальное хранилище потока для последнего сообщения об ошибке — у каждого потока собственное состояние ошибки. Функции инициализации или очистки не требуются.
Фаззинг
xmloxide включает цели для фаззинга при тестировании безопасности:
# Установка cargo-fuzz (требуется nightly)
cargo install cargo-fuzz
# Запуск цели фаззинга
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
Минимальная поддерживаемая версия Rust: 1.81
Ограничения
- Нет XML 1.1 — xmloxide реализует только XML 1.0 (пятое издание). XML 1.1 редко используется и не планируется.
- Нет XSLT — XSLT — отдельная спецификация (libxslt) и выходит за рамки.
- HTML-парсеры — предоставлены как парсер HTML 4.01 (поведение, соответствующее libxml2), так и полный парсер WHATWG HTML5. Парсер HTML5 проходит 100% html5lib-tests.
- Push-парсер буферизует внутри — API push/инкрементального парсера (
PushParser) в настоящее время буферизует все переданные данные и выполняет полный разбор при вызовеfinish(), а не потоковую передачу, какxmlParseChunkв libxml2. Потоковый SAX (parse_saxдля XML,html5::sax::parse_html5_saxдля HTML5) доступен как альтернатива для обработки больших документов с ограниченной памятью. - Ось
namespace::в XPath — осьnamespace::возвращает узел элемента, когда совпадают области видимости пространств имён (вместо создания отдельных узлов пространств имён), следуя той же схеме, что и ось атрибутов.
Участие
См. CONTRIBUTING.md для настройки разработки и рекомендаций.
Журнал изменений
См. CHANGELOG.md для истории версий.
Лицензия
MIT