#![allow(
missing_docs,
non_snake_case,
non_camel_case_types,
non_upper_case_globals
)]
use core::ffi::c_void;
use core::ptr;
use std::collections::HashMap;
use std::os::raw::{c_char, c_int};
use crate::abi::structs::*;
use crate::abi::types::xmlElementType::*;
use crate::xml::xpath::ast::CompiledExpr;
use crate::xml::xpath::context::XPathContext;
use crate::xml::xpath::types::XPathValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchematronPatternType {
Assert,
Report,
}
#[derive(Debug, Clone)]
pub struct SchematronPattern {
pub pattern_type: SchematronPatternType,
pub test: String,
pub compiled_test: Option<CompiledExpr>,
pub text: String,
pub flag: Option<String>,
pub role: Option<String>,
pub id: Option<String>,
pub icon: Option<String>,
pub see: Option<String>,
pub diagnostics: Option<String>,
}
impl SchematronPattern {
pub fn new(pattern_type: SchematronPatternType, test: String, text: String) -> Self {
Self {
pattern_type,
compiled_test: crate::xml::xpath::compile(&test),
test,
text,
flag: None,
role: None,
id: None,
icon: None,
see: None,
diagnostics: None,
}
}
}
#[derive(Debug, Clone)]
pub struct SchematronRule {
pub context: String,
pub compiled_context: Option<CompiledExpr>,
pub patterns: Vec<SchematronPattern>,
pub id: Option<String>,
pub abstract_: bool,
pub extends: Vec<String>,
}
impl SchematronRule {
pub fn new(context: String) -> Self {
Self {
compiled_context: crate::xml::xpath::compile(&context),
context,
patterns: Vec::new(),
id: None,
abstract_: false,
extends: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct SchematronPhase {
pub id: String,
pub active_patterns: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SchematronDiagnostic {
pub id: String,
pub text: String,
pub icon: Option<String>,
pub see: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SchematronSchema {
pub title: Option<String>,
pub phases: HashMap<String, SchematronPhase>,
pub rules: HashMap<String, SchematronRule>,
pub pattern_groups: HashMap<String, Vec<String>>,
pub pattern_order: Vec<String>,
pub ns: HashMap<String, String>,
pub query_binding: String,
pub default_phase: Option<String>,
pub diagnostics: HashMap<String, SchematronDiagnostic>,
pub errors: Vec<String>,
}
impl SchematronSchema {
pub fn new() -> Self {
Self {
title: None,
phases: HashMap::new(),
rules: HashMap::new(),
pattern_groups: HashMap::new(),
pattern_order: Vec::new(),
ns: HashMap::new(),
query_binding: "xslt".to_string(),
default_phase: None,
diagnostics: HashMap::new(),
errors: Vec::new(),
}
}
pub fn resolve_rule(&self, rule_id: &str) -> Option<SchematronRule> {
let rule = self.rules.get(rule_id)?.clone();
Some(self.resolve_extends(rule))
}
fn resolve_extends(&self, mut rule: SchematronRule) -> SchematronRule {
let extended_ids: Vec<String> = rule.extends.clone();
for ext_id in &extended_ids {
if let Some(ext_rule) = self.rules.get(ext_id) {
let resolved_ext = self.resolve_extends(ext_rule.clone());
rule.patterns.extend(resolved_ext.patterns);
}
}
rule
}
pub fn active_rules(&self, phase_id: Option<&str>) -> Vec<SchematronRule> {
let active_patterns: Vec<String> = match phase_id {
Some(pid) => {
if let Some(phase) = self.phases.get(pid) {
phase.active_patterns.clone()
} else {
self.pattern_order.clone()
}
}
None => {
if let Some(default) = &self.default_phase {
if let Some(phase) = self.phases.get(default) {
phase.active_patterns.clone()
} else {
self.pattern_order.clone()
}
} else {
self.pattern_order.clone()
}
}
};
let mut result = Vec::new();
for pat_id in &active_patterns {
if let Some(rule_ids) = self.pattern_groups.get(pat_id) {
for rule_id in rule_ids {
if let Some(rule) = self.rules.get(rule_id) {
if !rule.abstract_ {
result.push(self.resolve_extends(rule.clone()));
}
}
}
}
}
result
}
}
impl Default for SchematronSchema {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct SchematronValidCtxt {
pub schema: Option<SchematronSchema>,
pub errors: Vec<String>,
pub nb_errors: i32,
pub active_phase: Option<String>,
}
impl SchematronValidCtxt {
pub const fn new() -> Self {
Self {
schema: None,
errors: Vec::new(),
nb_errors: 0,
active_phase: None,
}
}
pub fn record_error(&mut self, msg: String) {
self.errors.push(msg);
self.nb_errors += 1;
}
}
impl Default for SchematronValidCtxt {
fn default() -> Self {
Self::new()
}
}
unsafe fn get_local_name(node: *mut _xmlNode) -> String {
if node.is_null() {
return String::new();
}
unsafe {
let name = (*node).name;
if name.is_null() {
return String::new();
}
let mut len = 0;
while *name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(name, len);
if let Ok(s) = std::str::from_utf8(slice) {
if let Some(pos) = s.find(':') {
s[pos + 1..].to_string()
} else {
s.to_string()
}
} else {
String::new()
}
}
}
unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
if node.is_null() {
return String::new();
}
unsafe {
let ns = (*node).ns;
let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
let mut len = 0;
while *(*ns).prefix.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts((*ns).prefix, len);
if let Ok(s) = std::str::from_utf8(slice) {
format!("{}:", s)
} else {
String::new()
}
} else {
String::new()
};
let name = (*node).name;
if name.is_null() {
return String::new();
}
let mut len = 0;
while *name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(name, len);
if let Ok(s) = std::str::from_utf8(slice) {
format!("{}{}", prefix, s)
} else {
String::new()
}
}
}
unsafe fn get_node_text(node: *mut _xmlNode) -> String {
if node.is_null() {
return String::new();
}
let mut result = String::new();
unsafe {
let mut child = (*node).children;
while !child.is_null() {
if ((*child).type_ == XML_TEXT_NODE as c_int
|| (*child).type_ == XML_CDATA_SECTION_NODE as c_int)
&& !(*child).content.is_null()
{
let content = (*child).content;
let mut len = 0;
while *content.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(content, len);
result.push_str(&String::from_utf8_lossy(slice));
}
child = (*child).next;
}
}
result
}
unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
if node.is_null() {
return None;
}
unsafe {
let mut prop = (*node).properties;
while !prop.is_null() {
let prop_name = (*prop).name;
if !prop_name.is_null() {
let mut len = 0;
while *prop_name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(prop_name, len);
if let Ok(s) = std::str::from_utf8(slice) {
if s == name {
return Some(get_node_text(prop as *mut _xmlNode));
}
}
}
prop = (*prop).next;
}
}
None
}
#[allow(dead_code)]
unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
if node.is_null() {
return false;
}
unsafe {
let name = (*node).name;
if name.is_null() {
return false;
}
let mut len = 0;
while *name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(name, len);
if let Ok(s) = std::str::from_utf8(slice) {
let local = if let Some(pos) = s.find(':') {
&s[pos + 1..]
} else {
s
};
return local == local_name;
}
}
false
}
#[allow(dead_code)]
unsafe fn child_elements(node: *mut _xmlNode) -> Vec<*mut _xmlNode> {
let mut children = Vec::new();
if node.is_null() {
return children;
}
unsafe {
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
children.push(child);
}
child = (*child).next;
}
}
children
}
unsafe fn get_inline_text(node: *mut _xmlNode) -> String {
if node.is_null() {
return String::new();
}
let mut result = String::new();
unsafe {
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_TEXT_NODE as c_int
|| (*child).type_ == XML_CDATA_SECTION_NODE as c_int
{
if !(*child).content.is_null() {
let content = (*child).content;
let mut len = 0;
while *content.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(content, len);
result.push_str(&String::from_utf8_lossy(slice));
}
} else if (*child).type_ == XML_ELEMENT_NODE as c_int {
let local = get_local_name(child);
match local.as_str() {
"span" | "emph" | "dir" => {
result.push_str(&get_inline_text(child));
}
_ => {}
}
}
child = (*child).next;
}
}
result
}
pub fn schematron_parse(xml_doc: &str) -> Result<SchematronSchema, String> {
let doc_ptr = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
xml_doc.as_ptr() as *const c_char,
xml_doc.len() as c_int,
c"schema.sch".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
if doc_ptr.is_null() {
return Err("Failed to parse Schematron schema XML document".to_string());
}
let result = unsafe { schematron_parse_doc(doc_ptr) };
unsafe {
crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
}
result
}
unsafe fn schematron_parse_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
unsafe {
let root = (*doc).children;
if root.is_null() {
return Err("Schematron document has no root element".to_string());
}
let mut root_elem = root;
while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
root_elem = (*root_elem).next;
}
if root_elem.is_null() {
return Err("Schematron document has no root element".to_string());
}
let local_name = get_local_name(root_elem);
if local_name != "schema" {
return Err(format!(
"Expected '<schema>' root element, found '<{}>'",
local_name
));
}
Ok(schematron_parse_schema_node(root_elem))
}
}
unsafe fn schematron_parse_schema_node(node: *mut _xmlNode) -> SchematronSchema {
unsafe {
let mut schema = SchematronSchema::new();
if let Some(qb) = get_attr(node, "queryBinding") {
schema.query_binding = qb;
}
schema.title = get_attr(node, "title");
if let Some(df) = get_attr(node, "defaultPhase") {
schema.default_phase = Some(df);
}
let mut current_pattern_id: Option<String> = None;
let mut pattern_names: HashMap<String, Vec<String>> = HashMap::new();
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let local = get_local_name(child);
match local.as_str() {
"title" => {
if schema.title.is_none() {
schema.title = Some(get_node_text(child).trim().to_string());
}
}
"ns" => {
let prefix = get_attr(child, "prefix").unwrap_or_default();
let uri = get_attr(child, "uri").unwrap_or_default();
if !prefix.is_empty() && !uri.is_empty() {
schema.ns.insert(prefix, uri);
}
}
"phase" => {
let phase = schematron_parse_phase(child);
schema.phases.insert(phase.id.clone(), phase);
}
"pattern" => {
let pat_id = schematron_parse_pattern_node(
child,
&mut schema,
&mut current_pattern_id,
&mut pattern_names,
);
current_pattern_id = pat_id;
}
"rule" => {
let rule = schematron_parse_rule(child, &mut schema);
let rule_id = rule
.id
.clone()
.unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
let rid = rule_id.clone();
schema.rules.insert(rid, rule);
if let Some(ref pid) = current_pattern_id {
schema
.pattern_groups
.entry(pid.clone())
.or_default()
.push(rule_id);
} else {
let anon_id = format!("_anon_{}", schema.pattern_order.len());
schema
.pattern_groups
.entry(anon_id.clone())
.or_default()
.push(rule_id);
if !schema.pattern_order.contains(&anon_id) {
schema.pattern_order.push(anon_id);
}
}
}
"diagnostics" => {
schematron_parse_diagnostics(child, &mut schema);
}
"include" => {
schematron_parse_include(child, &mut schema);
}
"p" | "caption" => {
}
_ => {
schema
.errors
.push(format!("Unexpected element '<{}>' in schema", local));
}
}
}
child = (*child).next;
}
schema
}
}
unsafe fn schematron_parse_pattern_node(
node: *mut _xmlNode,
schema: &mut SchematronSchema,
_current_pattern_id: &mut Option<String>,
_pattern_names: &mut HashMap<String, Vec<String>>,
) -> Option<String> {
unsafe {
let pat_id = get_attr(node, "id");
let pat_name = get_attr(node, "name");
let pat_is_a = get_attr(node, "is-a");
let pat_see = get_attr(node, "see");
let pat_icon = get_attr(node, "icon");
let pat_role = get_attr(node, "role");
let pid = pat_id
.clone()
.unwrap_or_else(|| format!("_pattern_{}", schema.pattern_order.len()));
let mut rule_ids: Vec<String> = Vec::new();
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let local = get_local_name(child);
match local.as_str() {
"rule" => {
let rule = schematron_parse_rule(child, schema);
let rule_id = rule
.id
.clone()
.unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
let rid = rule_id.clone();
schema.rules.insert(rid, rule);
rule_ids.push(rule_id);
}
"p" | "caption" => {
}
_ => {
schema
.errors
.push(format!("Unexpected element '<{}>' in pattern", local));
}
}
}
child = (*child).next;
}
schema.pattern_groups.insert(pid.clone(), rule_ids);
schema.pattern_order.push(pid.clone());
if pat_is_a.is_some() {
}
let _ = pat_name;
let _ = pat_see;
let _ = pat_icon;
let _ = pat_role;
Some(pid)
}
}
unsafe fn schematron_parse_rule(
node: *mut _xmlNode,
schema: &mut SchematronSchema,
) -> SchematronRule {
unsafe {
let context = get_attr(node, "context").unwrap_or_default();
let mut rule = SchematronRule::new(context);
rule.id = get_attr(node, "id");
let abs = get_attr(node, "abstract").unwrap_or_default();
rule.abstract_ = abs == "true" || abs == "1";
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let local = get_local_name(child);
match local.as_str() {
"assert" => {
let pattern = schematron_parse_assert(child, SchematronPatternType::Assert);
rule.patterns.push(pattern);
}
"report" => {
let pattern = schematron_parse_assert(child, SchematronPatternType::Report);
rule.patterns.push(pattern);
}
"extends" => {
if let Some(ext_rule) = get_attr(child, "rule") {
rule.extends.push(ext_rule);
}
}
"let" => {
let name = get_attr(child, "name").unwrap_or_default();
let value = get_attr(child, "value").unwrap_or_default();
if !name.is_empty() {
let _ = value;
}
}
"param" => {
let _name = get_attr(child, "name");
let _value = get_attr(child, "value");
}
"p" | "caption" => {
}
_ => {
schema
.errors
.push(format!("Unexpected element '<{}>' in rule", local));
}
}
}
child = (*child).next;
}
rule
}
}
unsafe fn schematron_parse_assert(
node: *mut _xmlNode,
pattern_type: SchematronPatternType,
) -> SchematronPattern {
unsafe {
let test = get_attr(node, "test").unwrap_or_default();
let text = get_inline_text(node);
let mut pattern = SchematronPattern::new(pattern_type, test, text);
pattern.flag = get_attr(node, "flag");
pattern.id = get_attr(node, "id");
pattern.icon = get_attr(node, "icon");
pattern.see = get_attr(node, "see");
pattern.role = get_attr(node, "role");
pattern.diagnostics = get_attr(node, "diagnostics");
pattern
}
}
unsafe fn schematron_parse_phase(node: *mut _xmlNode) -> SchematronPhase {
unsafe {
let id = get_attr(node, "id").unwrap_or_default();
let mut phase = SchematronPhase {
id,
active_patterns: Vec::new(),
};
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let local = get_local_name(child);
if local == "active" {
if let Some(pattern) = get_attr(child, "pattern") {
phase.active_patterns.push(pattern);
}
}
}
child = (*child).next;
}
phase
}
}
unsafe fn schematron_parse_diagnostics(node: *mut _xmlNode, schema: &mut SchematronSchema) {
unsafe {
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let local = get_local_name(child);
if local == "diagnostic" {
let diag = schematron_parse_diagnostic(child);
schema.diagnostics.insert(diag.id.clone(), diag);
}
}
child = (*child).next;
}
}
}
unsafe fn schematron_parse_diagnostic(node: *mut _xmlNode) -> SchematronDiagnostic {
unsafe {
let id = get_attr(node, "id").unwrap_or_default();
let text = get_inline_text(node);
let icon = get_attr(node, "icon");
let see = get_attr(node, "see");
SchematronDiagnostic {
id,
text,
icon,
see,
}
}
}
unsafe fn schematron_parse_include(node: *mut _xmlNode, _schema: &mut SchematronSchema) {
unsafe {
let href = get_attr(node, "href");
if let Some(url) = href {
let url_c = std::ffi::CString::new(url.clone()).ok();
if let Some(c) = url_c {
let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
if !doc.is_null() {
let mut root = (*doc).children;
while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
root = (*root).next;
}
if !root.is_null() {
let local = get_local_name(root);
if local == "schema" || local == "pattern" || local == "rule" {
}
}
crate::abi::exports_xml2::xmlFreeDoc(doc);
}
}
}
}
}
unsafe fn expand_diagnostic_message(
text: &str,
context_node: *mut _xmlNode,
xpath_ctxt: &mut XPathContext,
) -> String {
let _ = context_node;
let _ = xpath_ctxt;
text.to_string()
}
fn evaluate_xpath_boolean(
compiled: &CompiledExpr,
xpath_ctxt: &mut XPathContext,
) -> Result<bool, String> {
match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
Some(value) => Ok(value.as_boolean()),
None => Err("XPath evaluation failed".to_string()),
}
}
pub unsafe fn schematron_validate_doc(
schema: &SchematronSchema,
doc: *mut _xmlDoc,
ctxt: &mut SchematronValidCtxt,
) -> bool {
unsafe {
if doc.is_null() {
ctxt.record_error("Document is null".to_string());
return false;
}
let root = (*doc).children;
if root.is_null() {
ctxt.record_error("Document has no children".to_string());
return false;
}
let mut root_elem = root;
while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
root_elem = (*root_elem).next;
}
if root_elem.is_null() {
ctxt.record_error("Document has no root element".to_string());
return false;
}
let phase_id = ctxt.active_phase.as_deref();
let rules = schema.active_rules(phase_id);
if rules.is_empty() {
return true;
}
let mut xpath_ctxt = XPathContext::new(doc);
let core_funcs = crate::xml::xpath::functions::core_functions();
for (name, func) in core_funcs {
xpath_ctxt.register_function(&name, func);
}
for (prefix, uri) in &schema.ns {
xpath_ctxt.namespaces.insert(prefix.clone(), uri.clone());
}
let mut valid = true;
for rule in &rules {
let matching_nodes: Vec<*mut _xmlNode> =
find_matching_nodes(rule, root_elem, doc, &mut xpath_ctxt);
for context_node in &matching_nodes {
xpath_ctxt.set_context_node(*context_node);
for pattern in &rule.patterns {
let compiled = match &pattern.compiled_test {
Some(c) => c,
None => continue,
};
let test_result = match evaluate_xpath_boolean(compiled, &mut xpath_ctxt) {
Ok(val) => val,
Err(e) => {
ctxt.record_error(format!(
"XPath error in '{}' test '{}': {}",
if pattern.pattern_type == SchematronPatternType::Assert {
"assert"
} else {
"report"
},
pattern.test,
e
));
valid = false;
continue;
}
};
let message =
expand_diagnostic_message(&pattern.text, *context_node, &mut xpath_ctxt);
match pattern.pattern_type {
SchematronPatternType::Assert => {
if !test_result {
let node_name = get_node_qname(*context_node);
let flag_str = pattern
.flag
.as_ref()
.map(|f| format!(" [{}]", f))
.unwrap_or_default();
let role_str = pattern
.role
.as_ref()
.map(|r| format!(" ({})", r))
.unwrap_or_default();
let msg = if message.is_empty() {
format!(
"assertion failed: '{}' for node '{}'{}{}",
pattern.test, node_name, flag_str, role_str
)
} else {
format!(
"assertion '{}' failed for node '{}'{}{}: {}",
pattern.test, node_name, flag_str, role_str, message
)
};
ctxt.record_error(msg);
valid = false;
}
}
SchematronPatternType::Report => {
if test_result {
let node_name = get_node_qname(*context_node);
let flag_str = pattern
.flag
.as_ref()
.map(|f| format!(" [{}]", f))
.unwrap_or_default();
let role_str = pattern
.role
.as_ref()
.map(|r| format!(" ({})", r))
.unwrap_or_default();
let msg = if message.is_empty() {
format!(
"report triggered: '{}' for node '{}'{}{}",
pattern.test, node_name, flag_str, role_str
)
} else {
format!(
"report '{}' triggered for node '{}'{}{}: {}",
pattern.test, node_name, flag_str, role_str, message
)
};
ctxt.record_error(msg);
valid = false;
}
}
}
}
}
}
valid
}
}
unsafe fn find_matching_nodes(
rule: &SchematronRule,
root: *mut _xmlNode,
doc: *mut _xmlDoc,
xpath_ctxt: &mut XPathContext,
) -> Vec<*mut _xmlNode> {
unsafe {
if rule.context.is_empty() {
let mut nodes = Vec::new();
collect_all_elements(root, &mut nodes);
return nodes;
}
if let Some(compiled) = &rule.compiled_context {
let is_simple_name = !rule.context.contains('/')
&& !rule.context.contains("::")
&& !rule.context.contains('[')
&& !rule.context.contains('(');
if !is_simple_name {
xpath_ctxt.set_context_node(root);
xpath_ctxt.document = doc;
if let Some(XPathValue::NodeSet(ns)) =
crate::xml::xpath::evaluate(compiled, xpath_ctxt)
{
if !ns.is_empty() {
return ns.iter().collect();
}
}
}
simple_context_match(&rule.context, root)
} else {
simple_context_match(&rule.context, root)
}
}
}
fn simple_context_match(context: &str, root: *mut _xmlNode) -> Vec<*mut _xmlNode> {
unsafe {
let context = context.trim();
if context == "*" || context == "//*" {
let mut nodes = Vec::new();
collect_all_elements(root, &mut nodes);
return nodes;
}
if let Some(name) = context.strip_prefix("//") {
if name.is_empty() || name == "*" {
let mut nodes = Vec::new();
collect_all_elements(root, &mut nodes);
return nodes;
}
let mut nodes = Vec::new();
collect_elements_by_name(root, name, &mut nodes);
return nodes;
}
if !context.contains('/') && !context.contains("::") {
let mut nodes = Vec::new();
let root_qname = get_node_qname(root);
let root_local = get_local_name(root);
if root_qname == context || root_local == context || context == "*" {
nodes.push(root);
}
let mut child = (*root).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let qname = get_node_qname(child);
let local = get_local_name(child);
if qname == context || local == context || context == "*" {
nodes.push(child);
}
}
child = (*child).next;
}
return nodes;
}
vec![root]
}
}
unsafe fn collect_all_elements(node: *mut _xmlNode, nodes: &mut Vec<*mut _xmlNode>) {
unsafe {
if node.is_null() {
return;
}
if (*node).type_ == XML_ELEMENT_NODE as c_int {
nodes.push(node);
}
let mut child = (*node).children;
while !child.is_null() {
collect_all_elements(child, nodes);
child = (*child).next;
}
}
}
unsafe fn collect_elements_by_name(
node: *mut _xmlNode,
name: &str,
nodes: &mut Vec<*mut _xmlNode>,
) {
unsafe {
if node.is_null() {
return;
}
if (*node).type_ == XML_ELEMENT_NODE as c_int {
let qname = get_node_qname(node);
let local = get_local_name(node);
if qname == name || local == name {
nodes.push(node);
}
}
let mut child = (*node).children;
while !child.is_null() {
collect_elements_by_name(child, name, nodes);
child = (*child).next;
}
}
}
pub fn schematron_parse_schema(xml_doc: &str) -> Result<SchematronSchema, String> {
schematron_parse(xml_doc)
}
pub unsafe fn schematron_parse_schema_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
schematron_parse_doc(doc)
}
pub unsafe fn schematron_validate_doc_schema(
schema: &SchematronSchema,
doc: *mut _xmlDoc,
ctxt: &mut SchematronValidCtxt,
) -> bool {
schematron_validate_doc(schema, doc, ctxt)
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronNewParserCtxt(url: *const c_char) -> *mut c_void {
if url.is_null() {
return Box::into_raw(Box::new(SchematronSchema::new())) as *mut c_void;
}
let url_str = unsafe {
let mut len = 0;
while *url.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(url as *const u8, len);
String::from_utf8_lossy(slice).to_string()
};
if !url_str.is_empty() {
let url_c = std::ffi::CString::new(url_str.clone()).ok();
if let Some(c) = url_c {
let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
if !doc.is_null() {
let result = schematron_parse_doc(doc);
crate::abi::exports_xml2::xmlFreeDoc(doc);
if let Ok(schema) = result {
let schema_box = Box::new(schema);
return Box::into_raw(schema_box) as *mut c_void;
}
}
}
}
Box::into_raw(Box::new(SchematronSchema::new())) as *mut c_void
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronNewMemParserCtxt(
buffer: *const c_char,
size: c_int,
) -> *mut c_void {
if buffer.is_null() || size <= 0 {
return ptr::null_mut();
}
let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
let xml_str = String::from_utf8_lossy(buf_slice).to_string();
match schematron_parse(&xml_str) {
Ok(schema) => {
let schema_box = Box::new(schema);
Box::into_raw(schema_box) as *mut c_void
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub const unsafe extern "C" fn xmlSchematronParse(ctxt: *mut c_void) -> *mut c_void {
if ctxt.is_null() {
return ptr::null_mut();
}
ctxt
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronFree(schema: *mut c_void) {
if schema.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(schema as *mut SchematronSchema);
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronFreeParserCtxt(ctxt: *mut c_void) {
if ctxt.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(ctxt as *mut SchematronSchema);
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronNewValidCtxt(schema: *mut c_void) -> *mut c_void {
let mut ctxt = SchematronValidCtxt::new();
if !schema.is_null() {
unsafe {
let schema_ref = &*(schema as *const SchematronSchema);
ctxt.schema = Some(schema_ref.clone());
}
}
let boxed = Box::new(ctxt);
Box::into_raw(boxed) as *mut c_void
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronFreeValidCtxt(ctxt: *mut c_void) {
if ctxt.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(ctxt as *mut SchematronValidCtxt);
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
if ctxt.is_null() || doc.is_null() {
return -1;
}
unsafe {
let valid_ctxt = &mut *(ctxt as *mut SchematronValidCtxt);
let schema = match &valid_ctxt.schema {
Some(s) => s,
None => return -1,
};
let mut temp_ctxt = SchematronValidCtxt::new();
temp_ctxt.active_phase = valid_ctxt.active_phase.clone();
let valid = schematron_validate_doc(schema, doc, &mut temp_ctxt);
if valid {
0
} else {
valid_ctxt.errors = temp_ctxt.errors;
valid_ctxt.nb_errors = temp_ctxt.nb_errors;
temp_ctxt.nb_errors
}
}
}
pub type SchematronValidityErrorFunc = unsafe extern "C" fn(ctx: *mut c_void, msg: *const c_char);
pub type SchematronValidityWarningFunc = unsafe extern "C" fn(ctx: *mut c_void, msg: *const c_char);
#[derive(Clone, Copy)]
struct SchematronSendPtr(*mut c_void);
unsafe impl Send for SchematronSendPtr {}
unsafe impl Sync for SchematronSendPtr {}
impl Default for SchematronSendPtr {
fn default() -> Self {
SchematronSendPtr(core::ptr::null_mut())
}
}
#[derive(Clone, Copy, Default)]
struct SchematronParserState {
err: Option<SchematronValidityErrorFunc>,
warn: Option<SchematronValidityWarningFunc>,
ctx: SchematronSendPtr,
}
#[derive(Clone, Copy, Default)]
struct SchematronValidState {
err: Option<SchematronValidityErrorFunc>,
warn: Option<SchematronValidityWarningFunc>,
ctx: SchematronSendPtr,
options: c_int,
}
static SCHEMATRON_PARSER_STATE: once_cell::sync::Lazy<
parking_lot::Mutex<std::collections::HashMap<usize, SchematronParserState>>,
> = once_cell::sync::Lazy::new(Default::default);
static SCHEMATRON_VALID_STATE: once_cell::sync::Lazy<
parking_lot::Mutex<std::collections::HashMap<usize, SchematronValidState>>,
> = once_cell::sync::Lazy::new(Default::default);
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronSetParserErrors(
ctxt: *mut c_void,
err: Option<SchematronValidityErrorFunc>,
warn: Option<SchematronValidityWarningFunc>,
ctx: *mut c_void,
) {
if ctxt.is_null() {
return;
}
let mut map = SCHEMATRON_PARSER_STATE.lock();
let st = map.entry(ctxt as usize).or_default();
st.err = err;
st.warn = warn;
st.ctx = SchematronSendPtr(ctx);
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronGetParserErrors(
ctxt: *mut c_void,
err: *mut Option<SchematronValidityErrorFunc>,
warn: *mut Option<SchematronValidityWarningFunc>,
ctx: *mut *mut c_void,
) -> c_int {
if ctxt.is_null() {
return -1;
}
let map = SCHEMATRON_PARSER_STATE.lock();
let st = map.get(&(ctxt as usize)).copied().unwrap_or_default();
if !err.is_null() {
*err = st.err;
}
if !warn.is_null() {
*warn = st.warn;
}
if !ctx.is_null() {
*ctx = st.ctx.0;
}
0
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronSetValidErrors(
ctxt: *mut c_void,
err: Option<SchematronValidityErrorFunc>,
warn: Option<SchematronValidityWarningFunc>,
ctx: *mut c_void,
) {
if ctxt.is_null() {
return;
}
let mut map = SCHEMATRON_VALID_STATE.lock();
let st = map.entry(ctxt as usize).or_default();
st.err = err;
st.warn = warn;
st.ctx = SchematronSendPtr(ctx);
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronGetValidErrors(
ctxt: *mut c_void,
err: *mut Option<SchematronValidityErrorFunc>,
warn: *mut Option<SchematronValidityWarningFunc>,
ctx: *mut *mut c_void,
) -> c_int {
if ctxt.is_null() {
return -1;
}
let map = SCHEMATRON_VALID_STATE.lock();
let st = map.get(&(ctxt as usize)).copied().unwrap_or_default();
if !err.is_null() {
*err = st.err;
}
if !warn.is_null() {
*warn = st.warn;
}
if !ctx.is_null() {
*ctx = st.ctx.0;
}
0
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronSetValidOptions(ctxt: *mut c_void, options: c_int) -> c_int {
if ctxt.is_null() {
return -1;
}
let mut map = SCHEMATRON_VALID_STATE.lock();
let st = map.entry(ctxt as usize).or_default();
let old = st.options;
st.options = options;
old
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronValidCtxtGetOptions(ctxt: *mut c_void) -> c_int {
if ctxt.is_null() {
return -1;
}
SCHEMATRON_VALID_STATE
.lock()
.get(&(ctxt as usize))
.map_or(0, |st| st.options)
}
#[no_mangle]
pub const unsafe extern "C" fn xmlSchematronIsValid(ctxt: *mut c_void) -> c_int {
if ctxt.is_null() {
return 0;
}
unsafe {
let vc = &*(ctxt as *const SchematronValidCtxt);
if vc.nb_errors > 0 {
0
} else {
1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlSchematronValidateOneElement(
ctxt: *mut c_void,
elem: *mut _xmlNode,
) -> c_int {
if ctxt.is_null() || elem.is_null() {
return -1;
}
unsafe {
let valid_ctxt = &mut *(ctxt as *mut SchematronValidCtxt);
let schema = match &valid_ctxt.schema {
Some(s) => s,
None => return -1,
};
let doc = (*elem).doc;
if doc.is_null() {
return -1;
}
let mut temp_ctxt = SchematronValidCtxt::new();
temp_ctxt.active_phase = valid_ctxt.active_phase.clone();
let valid = schematron_validate_doc(schema, doc, &mut temp_ctxt);
if !valid {
valid_ctxt.errors = temp_ctxt.errors;
valid_ctxt.nb_errors = temp_ctxt.nb_errors;
}
if valid {
0
} else {
-1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_schema() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="count(*) > 0">Root must have children</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
assert_eq!(schema.pattern_order.len(), 1);
assert_eq!(schema.rules.len(), 1);
}
#[test]
fn test_parse_with_ns() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<ns prefix="doc" uri="http://example.com/doc"/>
<pattern id="P1">
<rule context="doc:entry">
<assert test="doc:title">Entry must have a title</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
assert!(schema.ns.contains_key("doc"));
assert_eq!(schema.ns.get("doc").unwrap(), "http://example.com/doc");
}
#[test]
fn test_parse_with_phases() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
<phase id="phaseA">
<active pattern="P1"/>
</phase>
<phase id="phaseB">
<active pattern="P2"/>
</phase>
<pattern id="P1">
<rule context="root">
<assert test="true()">Always passes</assert>
</rule>
</pattern>
<pattern id="P2">
<rule context="root">
<assert test="false()">Always fails</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
assert_eq!(schema.phases.len(), 2);
assert!(schema.phases.contains_key("phaseA"));
assert!(schema.phases.contains_key("phaseB"));
assert_eq!(schema.default_phase.as_deref(), Some("phaseA"));
}
#[test]
fn test_parse_report_pattern() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<report test="@deprecated">Element is deprecated</report>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
let rule = schema.rules.values().next().unwrap();
assert_eq!(rule.patterns.len(), 1);
assert_eq!(rule.patterns[0].pattern_type, SchematronPatternType::Report);
assert_eq!(rule.patterns[0].test, "@deprecated");
}
#[test]
fn test_parse_abstract_rule() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule id="abstractRule" abstract="true" context="*">
<assert test="true()">Abstract assertion</assert>
</rule>
<rule id="concreteRule" context="root">
<extends rule="abstractRule"/>
<assert test="count(*) > 0">Concrete assertion</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
assert!(schema.rules.contains_key("abstractRule"));
assert!(schema.rules.contains_key("concreteRule"));
let abstract_rule = &schema.rules["abstractRule"];
assert!(abstract_rule.abstract_);
let concrete_rule = &schema.rules["concreteRule"];
assert!(!concrete_rule.abstract_);
assert_eq!(concrete_rule.extends.len(), 1);
assert_eq!(concrete_rule.extends[0], "abstractRule");
}
#[test]
fn test_parse_with_diagnostics() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<diagnostics>
<diagnostic id="diag1">This is a diagnostic message</diagnostic>
</diagnostics>
<pattern id="P1">
<rule context="root">
<assert test="true()" diagnostics="diag1">Assertion with diagnostic</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
assert!(schema.diagnostics.contains_key("diag1"));
assert_eq!(
schema.diagnostics["diag1"].text,
"This is a diagnostic message"
);
}
#[test]
fn test_parse_with_attributes() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron" title="Test Schema">
<pattern id="P1">
<rule context="root">
<assert test="true()" flag="warn" role="error" id="a1" icon="info" see="http://example.com">
Test message
</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
assert_eq!(schema.title.as_deref(), Some("Test Schema"));
let rule = schema.rules.values().next().unwrap();
let pat = &rule.patterns[0];
assert_eq!(pat.flag.as_deref(), Some("warn"));
assert_eq!(pat.role.as_deref(), Some("error"));
assert_eq!(pat.id.as_deref(), Some("a1"));
assert_eq!(pat.icon.as_deref(), Some("info"));
assert_eq!(pat.see.as_deref(), Some("http://example.com"));
}
#[test]
fn test_parse_empty_schema() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse empty schema");
let schema = result.unwrap();
assert!(schema.rules.is_empty());
assert!(schema.phases.is_empty());
}
#[test]
fn test_parse_no_assertions() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema with no assertions");
let schema = result.unwrap();
let rule = schema.rules.values().next().unwrap();
assert!(rule.patterns.is_empty());
}
#[test]
fn test_parse_invalid_root_element() {
let schema_xml = r#"<?xml version="1.0"?>
<not-schema xmlns="http://purl.oclc.org/dsdl/schematron">
</not-schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_err(), "Should fail with wrong root element");
assert!(
result.err().unwrap().contains("Expected '<schema>'"),
"Error should mention expected schema element"
);
}
#[test]
fn test_parse_empty_document_fails() {
let result = schematron_parse("");
assert!(result.is_err());
}
#[test]
fn test_parse_invalid_xml_fails() {
let result = schematron_parse("not valid xml <<<");
assert!(result.is_err());
}
#[test]
fn test_parse_schema_with_let_and_param() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<let name="x" value="42"/>
<param name="debug" value="true"/>
<assert test="true()">Test with let and param</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(
result.is_ok(),
"Failed to parse schema with let/param: {:?}",
result.err()
);
let schema = result.unwrap();
assert_eq!(schema.rules.len(), 1);
}
#[test]
fn test_parse_schema_with_documentation() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<p>This is documentation</p>
<caption>Table caption</caption>
<pattern id="P1">
<p>Pattern documentation</p>
<rule context="root">
<p>Rule documentation</p>
<assert test="true()">Real assertion</assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema with documentation");
let schema = result.unwrap();
assert_eq!(schema.rules.len(), 1);
}
#[test]
fn test_validate_assert_pass() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="true()">Always passes</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null(), "Failed to parse document");
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_assert_fail() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="false()">Always fails</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(
!valid,
"Validation should have failed, errors: {:?}",
ctxt.errors
);
assert!(ctxt.nb_errors > 0);
}
#[test]
fn test_validate_report_pass() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<report test="false()">Report should not trigger</report>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Report should not trigger: {:?}", ctxt.errors);
}
#[test]
fn test_validate_report_fail() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<report test="true()">Report should trigger</report>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid, "Report should have triggered");
assert!(ctxt.nb_errors > 0);
}
#[test]
fn test_validate_context_matching() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="child">
<assert test="true()">Child matches</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>
<child>A</child>
<child>B</child>
</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Context matching failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_multiple_rules() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="true()">Root passes</assert>
</rule>
</pattern>
<pattern id="P2">
<rule context="child">
<assert test="true()">Child passes</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>
<child>Content</child>
</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Multiple rules failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_with_phase_filtering() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
<phase id="phaseA">
<active pattern="P1"/>
</phase>
<phase id="phaseB">
<active pattern="P2"/>
</phase>
<pattern id="P1">
<rule context="root">
<assert test="true()">Always passes</assert>
</rule>
</pattern>
<pattern id="P2">
<rule context="root">
<assert test="false()">Always fails</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(
valid,
"Phase filtering should make validation pass: {:?}",
ctxt.errors
);
}
#[test]
fn test_validate_no_rules() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Empty schema should pass validation");
}
#[test]
fn test_validate_extends_resolution() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule id="base" abstract="true" context="*">
<assert test="true()">Base assertion</assert>
</rule>
<rule id="derived" context="root">
<extends rule="base"/>
<assert test="true()">Derived assertion</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let resolved = schema.resolve_rule("derived");
assert!(resolved.is_some());
let resolved = resolved.unwrap();
assert_eq!(
resolved.patterns.len(),
2,
"Should have inherited the base pattern"
);
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Extends resolution failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_assert_with_flag() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="false()" flag="warn">Warning message</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid);
assert!(ctxt.nb_errors > 0);
assert!(
ctxt.errors[0].contains("[warn]"),
"Error should include flag"
);
}
#[test]
fn test_c_abi_new_free_parser_ctxt() {
let ctxt = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
assert!(!ctxt.is_null());
unsafe { xmlSchematronFreeParserCtxt(ctxt) };
}
#[test]
fn test_c_abi_new_free_valid_ctxt() {
let schema = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
assert!(!schema.is_null());
let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
assert!(!valid_ctxt.is_null());
unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
unsafe { xmlSchematronFreeParserCtxt(schema) };
}
#[test]
fn test_c_abi_parse_free() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="true()">Test</assert>
</rule>
</pattern>
</schema>"#;
let ctxt = unsafe {
xmlSchematronNewMemParserCtxt(
schema_xml.as_ptr() as *const c_char,
schema_xml.len() as c_int,
)
};
assert!(!ctxt.is_null());
let schema = unsafe { xmlSchematronParse(ctxt) };
assert!(!schema.is_null());
unsafe { xmlSchematronFree(schema) };
}
#[test]
fn test_c_abi_validate_doc() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="true()">Always passes</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let ctxt = unsafe {
xmlSchematronNewMemParserCtxt(
schema_xml.as_ptr() as *const c_char,
schema_xml.len() as c_int,
)
};
assert!(!ctxt.is_null());
let schema = unsafe { xmlSchematronParse(ctxt) };
assert!(!schema.is_null());
let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
assert!(!valid_ctxt.is_null());
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
assert_eq!(result, 0, "Validation should pass (return 0)");
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
unsafe { xmlSchematronFree(schema) };
}
#[test]
fn test_c_abi_validate_fail() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="false()">Always fails</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let ctxt = unsafe {
xmlSchematronNewMemParserCtxt(
schema_xml.as_ptr() as *const c_char,
schema_xml.len() as c_int,
)
};
assert!(!ctxt.is_null());
let schema = unsafe { xmlSchematronParse(ctxt) };
assert!(!schema.is_null());
let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
assert!(!valid_ctxt.is_null());
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
assert!(result > 0, "Validation should fail (return > 0)");
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
unsafe { xmlSchematronFree(schema) };
}
#[test]
fn test_c_abi_null_handling() {
unsafe { xmlSchematronFree(ptr::null_mut()) };
unsafe { xmlSchematronFreeParserCtxt(ptr::null_mut()) };
unsafe { xmlSchematronFreeValidCtxt(ptr::null_mut()) };
let result = unsafe { xmlSchematronParse(ptr::null_mut()) };
assert!(result.is_null());
let result = unsafe { xmlSchematronValidateDoc(ptr::null_mut(), ptr::null_mut()) };
assert_eq!(result, -1);
}
#[test]
fn test_validate_null_doc() {
let schema = SchematronSchema::new();
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
assert!(!valid);
assert!(ctxt.nb_errors > 0);
}
#[test]
fn test_active_rules_default_phase() {
let mut schema = SchematronSchema::new();
let rule = SchematronRule::new("root".to_string());
schema.rules.insert("r1".to_string(), rule);
schema
.pattern_groups
.insert("p1".to_string(), vec!["r1".to_string()]);
schema.pattern_order.push("p1".to_string());
let rules = schema.active_rules(None);
assert_eq!(rules.len(), 1);
}
#[test]
fn test_active_rules_unknown_phase() {
let mut schema = SchematronSchema::new();
let rule = SchematronRule::new("root".to_string());
schema.rules.insert("r1".to_string(), rule);
schema
.pattern_groups
.insert("p1".to_string(), vec!["r1".to_string()]);
schema.pattern_order.push("p1".to_string());
let rules = schema.active_rules(Some("nonexistent"));
assert_eq!(rules.len(), 1, "Unknown phase should use all patterns");
}
#[test]
fn test_parse_schema_with_span_and_emph() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="true()">Message with <span class="x">inline</span> and <emph>emphasis</emph></assert>
</rule>
</pattern>
</schema>"#;
let result = schematron_parse(schema_xml);
assert!(
result.is_ok(),
"Failed to parse schema with span/emph: {:?}",
result.err()
);
let schema = result.unwrap();
let rule = schema.rules.values().next().unwrap();
let pat = &rule.patterns[0];
assert!(
pat.text.contains("inline"),
"Text should include span content"
);
assert!(
pat.text.contains("emphasis"),
"Text should include emph content"
);
}
#[test]
fn test_schematron_pattern_new_assert() {
let pat = SchematronPattern::new(
SchematronPatternType::Assert,
"true()".to_string(),
"Test message".to_string(),
);
assert_eq!(pat.pattern_type, SchematronPatternType::Assert);
assert_eq!(pat.test, "true()");
assert_eq!(pat.text, "Test message");
assert!(pat.compiled_test.is_some());
}
#[test]
fn test_schematron_pattern_new_report() {
let pat = SchematronPattern::new(
SchematronPatternType::Report,
"false()".to_string(),
"Report message".to_string(),
);
assert_eq!(pat.pattern_type, SchematronPatternType::Report);
assert!(pat.compiled_test.is_some());
}
#[test]
fn test_schematron_rule_new() {
let rule = SchematronRule::new("root".to_string());
assert_eq!(rule.context, "root");
assert!(rule.patterns.is_empty());
assert!(!rule.abstract_);
}
#[test]
fn test_schematron_schema_new() {
let schema = SchematronSchema::new();
assert_eq!(schema.query_binding, "xslt");
assert!(schema.rules.is_empty());
assert!(schema.phases.is_empty());
assert!(schema.ns.is_empty());
}
#[test]
fn test_schematron_valid_ctxt_new() {
let ctxt = SchematronValidCtxt::new();
assert!(ctxt.errors.is_empty());
assert_eq!(ctxt.nb_errors, 0);
assert!(ctxt.active_phase.is_none());
}
#[test]
fn test_validate_assert_with_child_count() {
let schema_xml = r#"<?xml version="1.0"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern id="P1">
<rule context="root">
<assert test="count(*) > 0">Root must have at least one child element</assert>
</rule>
</pattern>
</schema>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>
<child>Content</child>
</root>"#;
let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = SchematronValidCtxt::new();
let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Child count check failed: {:?}", ctxt.errors);
}
}