
إعادة تنفيذ خالص بلغة Rust لمكتبة libxml2
إعادة تنفيذ نقي بلغة Rust لمكتبة libxml2 — مكتبة تحليل XML/HTML القياسية الفعلية في عالم المصادر المفتوحة.
أصبحت libxml2 غير مدعومة رسميًا منذ ديسمبر 2025 مع مشاكل أمنية معروفة. تهدف xmloxide إلى أن تكون بديلاً آمنًا للذاكرة وعالي الأداء ينجح في نفس مجموعات اختبار المطابقة.
unsafe في الواجهة العامةhtml5::sax) تغلف المحلل دون بناء شجرة DOMcss::select) تشمل الأدوات المركبة والفئات الزائفة والبحث السريع بـ #idmatches()، replace()، tokenize()، upper-case()، lower-case()، abs()، min()، max()، والمزيد)serde لتسلسل/إلغاء تسلسل XML من/إلى أنواع Rustasync للتحليل من مصادر tokio::io::AsyncReadxmllint — أداة سطر أوامر لتحليل XML والتحقق منه والاستعلام عنهDocument مستقل ويمكن إرساله (Send) ومشاركته بين الخيوط (Sync)include/xmloxide.h) للتضمين في مشاريع C/C++encoding_rs (المكتبة ليس لها تبعيات أخرى؛ clap مخصص لسطر الأوامر فقط)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!("Element: {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"));
كما أن تحليل القطع (الخوارزمية التي تقف وراء 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();
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);
}
# تحليل وطباعة منسقة
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
إنتاجية التحليل تنافسية مع libxml2 — ضمن 3-4% في معظم المستندات، وأسرع بنسبة 12% في SVG. التسلسل أسرع بين 1.5 و 2.4 مرة بفضل تصميم الشجرة القائمة على الساحة. XPath أسرع بين 1.1 و 2.7 مرة عبر جميع المعايير.
التحليل:
التسلسل:
XPath:
التحسينات الرئيسية: شجرة قائمة على الساحة للتسلسل السريع، فحوصات مسبقة على مستوى البايت للتحقق من الأحرف، مسح نصي شامل، مسارات سريعة لـ ASCII لتحليل الأسماء، تقسيم أسماء العناصر بدون نسخ، حل الكيانات المضمنة، دمج خطوات XPath // مع توسيع المحاور المدمجة، موصلات شجرة مضمنة، ومسارات سريعة لاختبار الأسماء لمحاور الطفل/السليل.
# تشغيل المعايير (يتطلب مكتبة libxml2 النظامية)
cargo bench --features bench-libxml2 --bench comparison_bench
cargo test --all-features
توفر xmloxide واجهة متوافقة مع C لتضمينها في مشاريع 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، المحلل الدفعي، وكتالوجات XML — مُعلنة في include/xmloxide.h.
سلامة الخيوط: على عكس libxml2، لا تحتوي xmloxide على حالة عمومية. كل Document مستقل ويمكن إرساله ومشاركته بين الخيوط. تستخدم طبقة 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
PushParser) تقوم حالياً بتخزين جميع البيانات المدفوعة وتنفيذ التحليل الكامل عند finish()، بدلاً من التدفق الحقيقي مثل xmlParseChunk في libxml2. يتوفر دفق SAX (parse_sax لـ XML و html5::sax::parse_html5_sax لـ HTML5) كبديل لمعالجة المستندات الكبيرة المقيدة بالذاكرة.namespace:: — يعيد محور namespace:: عقدة العنصر عندما تتطابق المساحات الاسمية في النطاق (بدلاً من إنشاء عقد مساحة اسمية منفصلة)، متبعاً نفس نمط محور السمة.انظر CONTRIBUTING.md لإعدادات التطوير والإرشادات.
انظر CHANGELOG.md لتاريخ الإصدارات.
MIT
| الوحدة | الوصف |
|---|
tree | شجرة DOM قائمة على الساحة (Document، NodeId، NodeKind) |
parser | محلل تنازلي متكرر لـ XML 1.0 مع استرداد الأخطاء |
parser::push | محلل دفعي/متزايد للإدخال المجزأ |
html | محلل HTML 4.01 متسامح مع الأخطاء |
html5 | محلل WHATWG HTML الحي (محلل الرموز + باني الشجرة) |
html5::sax | واجهة دفق شبيهة بـ SAX لـ HTML5 (لا تُبنى شجرة DOM) |
css | محرك محددات CSS للاستعلام عن أشجار المستندات |
sax | محلل دفق يعتمد على الأحداث بتقنية SAX2 |
reader | واجهة تحليل سحب XmlReader |
serial | مسلسلات XML وHTML وHTML5، بالإضافة إلى XML قانوني (C14N) |
xpath | محلل ومقيّم تعبيرات XPath 1.0+ |
validation::dtd | تحليل DTD والتحقق منه |
validation::relaxng | التحقق من مخطط RelaxNG |
validation::xsd | التحقق من XML Schema (XSD) |
validation::schematron | التحقق القائم على قواعد ISO Schematron |
serde_xml | تسلسل/إلغاء تسلسل Serde لـ XML (ميزة اختيارية serde) |
async_xml | تحليل غير متزامن عبر tokio::io::AsyncRead (ميزة اختيارية async) |
xinclude | تضمين المستندات وفقًا لـ XInclude 1.0 |
catalog | كتالوجات XML من OASIS لحل URIs |
encoding | كشف ترميز الأحرف وتحويله |
ffi | روابط FFI بلغة C/C++ (include/xmloxide.h) |
| المستند | الحجم | xmloxide | libxml2 | النتيجة |
|---|
| خلاصة Atom | 4.9 كيلوبايت | 26.7 ميكروثانية (176 ميجابايت/ثانية) | 25.5 ميكروثانية (184 ميجابايت/ثانية) | أبطأ بنحو 4% |
| رسم SVG | 6.3 كيلوبايت | 58.5 ميكروثانية (103 ميجابايت/ثانية) | 65.6 ميكروثانية (92 ميجابايت/ثانية) | أسرع بنسبة 12% |
| ملف Maven POM | 11.5 كيلوبايت | 76.9 ميكروثانية (142 ميجابايت/ثانية) | 74.2 ميكروثانية (148 ميجابايت/ثانية) | أبطأ بنحو 4% |
| صفحة XHTML | 10.2 كيلوبايت | 69.5 ميكروثانية (139 ميجابايت/ثانية) | 61.5 ميكروثانية (157 ميجابايت/ثانية) | أبطأ بنحو 13% |
| كبير (374 كيلوبايت) | 374 كيلوبايت | 2.15 ملي ثانية (169 ميجابايت/ثانية) | 2.08 ملي ثانية (175 ميجابايت/ثانية) | أبطأ بنحو 3% |
| المستند | الحجم | xmloxide | libxml2 | النتيجة |
|---|
| خلاصة Atom | 4.9 كيلوبايت | 11.3 ميكروثانية | 17.5 ميكروثانية | أسرع بـ 1.5 مرة |
| ملف Maven POM | 11.5 كيلوبايت | 20.1 ميكروثانية | 47.5 ميكروثانية | أسرع بـ 2.4 مرة |
| كبير (374 كيلوبايت) | 374 كيلوبايت | 614 ميكروثانية | 1397 ميكروثانية | أسرع بـ 2.3 مرة |
| التعبير | xmloxide | libxml2 | النتيجة |
|---|
مسار بسيط (//entry/title) | 1.51 ميكروثانية | 1.63 ميكروثانية | أسرع بنسبة 8% |
مسند سمة (//book[@id]) | 5.91 ميكروثانية | 15.99 ميكروثانية | أسرع بـ 2.7 مرة |
دالة count() | 1.09 ميكروثانية | 1.67 ميكروثانية | أسرع بـ 1.5 مرة |
دالة string() | 1.32 ميكروثانية | 1.77 ميكروثانية | أسرع بـ 1.3 مرة |
| 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 |