1#![allow(
29 missing_docs,
30 non_snake_case,
31 non_camel_case_types,
32 non_upper_case_globals
33)]
34
35use core::ffi::c_void;
36use core::ptr;
37use std::collections::HashMap;
38use std::os::raw::{c_char, c_int};
39
40use crate::abi::allocator;
41use crate::abi::structs::*;
42use crate::abi::types::xmlElementType::*;
43use crate::abi::types::*;
44use crate::xml::xpath::ast::CompiledExpr;
45use crate::xml::xpath::context::XPathContext;
46use crate::xml::xpath::types::XPathValue;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum SchematronPatternType {
55 Assert,
57 Report,
59}
60
61#[derive(Debug, Clone)]
67pub struct SchematronPattern {
68 pub pattern_type: SchematronPatternType,
70 pub test: String,
72 pub compiled_test: Option<CompiledExpr>,
74 pub text: String,
76 pub flag: Option<String>,
78 pub role: Option<String>,
80 pub id: Option<String>,
82 pub icon: Option<String>,
84 pub see: Option<String>,
86 pub diagnostics: Option<String>,
88}
89
90impl SchematronPattern {
91 pub fn new(pattern_type: SchematronPatternType, test: String, text: String) -> Self {
93 Self {
94 pattern_type,
95 compiled_test: crate::xml::xpath::compile(&test),
96 test,
97 text,
98 flag: None,
99 role: None,
100 id: None,
101 icon: None,
102 see: None,
103 diagnostics: None,
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
118pub struct SchematronRule {
119 pub context: String,
121 pub compiled_context: Option<CompiledExpr>,
123 pub patterns: Vec<SchematronPattern>,
125 pub id: Option<String>,
127 pub abstract_: bool,
129 pub extends: Vec<String>,
131}
132
133impl SchematronRule {
134 pub fn new(context: String) -> Self {
136 Self {
137 compiled_context: crate::xml::xpath::compile(&context),
138 context,
139 patterns: Vec::new(),
140 id: None,
141 abstract_: false,
142 extends: Vec::new(),
143 }
144 }
145}
146
147#[derive(Debug, Clone)]
157pub struct SchematronPhase {
158 pub id: String,
160 pub active_patterns: Vec<String>,
162}
163
164#[derive(Debug, Clone)]
170pub struct SchematronDiagnostic {
171 pub id: String,
173 pub text: String,
175 pub icon: Option<String>,
177 pub see: Option<String>,
179}
180
181#[derive(Debug, Clone)]
191pub struct SchematronSchema {
192 pub title: Option<String>,
194 pub phases: HashMap<String, SchematronPhase>,
196 pub rules: HashMap<String, SchematronRule>,
198 pub pattern_groups: HashMap<String, Vec<String>>,
200 pub pattern_order: Vec<String>,
202 pub ns: HashMap<String, String>,
204 pub query_binding: String,
206 pub default_phase: Option<String>,
208 pub diagnostics: HashMap<String, SchematronDiagnostic>,
210 pub errors: Vec<String>,
212}
213
214impl SchematronSchema {
215 pub fn new() -> Self {
217 Self {
218 title: None,
219 phases: HashMap::new(),
220 rules: HashMap::new(),
221 pattern_groups: HashMap::new(),
222 pattern_order: Vec::new(),
223 ns: HashMap::new(),
224 query_binding: "xslt".to_string(),
225 default_phase: None,
226 diagnostics: HashMap::new(),
227 errors: Vec::new(),
228 }
229 }
230
231 pub fn resolve_rule(&self, rule_id: &str) -> Option<SchematronRule> {
233 let rule = self.rules.get(rule_id)?.clone();
234 Some(self.resolve_extends(rule))
235 }
236
237 fn resolve_extends(&self, mut rule: SchematronRule) -> SchematronRule {
239 let extended_ids: Vec<String> = rule.extends.clone();
240 for ext_id in &extended_ids {
241 if let Some(ext_rule) = self.rules.get(ext_id) {
242 let resolved_ext = self.resolve_extends(ext_rule.clone());
244 rule.patterns.extend(resolved_ext.patterns);
245 }
246 }
247 rule
248 }
249
250 pub fn active_rules(&self, phase_id: Option<&str>) -> Vec<SchematronRule> {
254 let active_patterns: Vec<String> = match phase_id {
256 Some(pid) => {
257 if let Some(phase) = self.phases.get(pid) {
258 phase.active_patterns.clone()
259 } else {
260 self.pattern_order.clone()
262 }
263 }
264 None => {
265 if let Some(default) = &self.default_phase {
267 if let Some(phase) = self.phases.get(default) {
268 phase.active_patterns.clone()
269 } else {
270 self.pattern_order.clone()
271 }
272 } else {
273 self.pattern_order.clone()
274 }
275 }
276 };
277
278 let mut result = Vec::new();
279 for pat_id in &active_patterns {
280 if let Some(rule_ids) = self.pattern_groups.get(pat_id) {
281 for rule_id in rule_ids {
282 if let Some(rule) = self.rules.get(rule_id) {
283 if !rule.abstract_ {
284 result.push(self.resolve_extends(rule.clone()));
285 }
286 }
287 }
288 }
289 }
290
291 result
292 }
293}
294
295impl Default for SchematronSchema {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301#[derive(Debug)]
310pub struct SchematronValidCtxt {
311 pub schema: Option<SchematronSchema>,
313 pub errors: Vec<String>,
315 pub nb_errors: i32,
317 pub active_phase: Option<String>,
319}
320
321impl SchematronValidCtxt {
322 pub fn new() -> Self {
324 Self {
325 schema: None,
326 errors: Vec::new(),
327 nb_errors: 0,
328 active_phase: None,
329 }
330 }
331
332 pub fn record_error(&mut self, msg: String) {
334 self.errors.push(msg);
335 self.nb_errors += 1;
336 }
337}
338
339impl Default for SchematronValidCtxt {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345unsafe fn get_local_name(node: *mut _xmlNode) -> String {
355 if node.is_null() {
356 return String::new();
357 }
358 unsafe {
359 let name = (*node).name;
360 if name.is_null() {
361 return String::new();
362 }
363 let mut len = 0;
364 while *name.add(len) != 0 {
365 len += 1;
366 }
367 let slice = std::slice::from_raw_parts(name, len);
368 if let Ok(s) = std::str::from_utf8(slice) {
369 if let Some(pos) = s.find(':') {
370 s[pos + 1..].to_string()
371 } else {
372 s.to_string()
373 }
374 } else {
375 String::new()
376 }
377 }
378}
379
380unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
386 if node.is_null() {
387 return String::new();
388 }
389 unsafe {
390 let ns = (*node).ns;
391 let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
392 let mut len = 0;
393 while *(*ns).prefix.add(len) != 0 {
394 len += 1;
395 }
396 let slice = std::slice::from_raw_parts((*ns).prefix, len);
397 if let Ok(s) = std::str::from_utf8(slice) {
398 format!("{}:", s)
399 } else {
400 String::new()
401 }
402 } else {
403 String::new()
404 };
405
406 let name = (*node).name;
407 if name.is_null() {
408 return String::new();
409 }
410 let mut len = 0;
411 while *name.add(len) != 0 {
412 len += 1;
413 }
414 let slice = std::slice::from_raw_parts(name, len);
415 if let Ok(s) = std::str::from_utf8(slice) {
416 format!("{}{}", prefix, s)
417 } else {
418 String::new()
419 }
420 }
421}
422
423unsafe fn get_node_text(node: *mut _xmlNode) -> String {
429 if node.is_null() {
430 return String::new();
431 }
432 let mut result = String::new();
433 unsafe {
434 let mut child = (*node).children;
435 while !child.is_null() {
436 if (*child).type_ == XML_TEXT_NODE as c_int
437 || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
438 {
439 if !(*child).content.is_null() {
440 let content = (*child).content;
441 let mut len = 0;
442 while *content.add(len) != 0 {
443 len += 1;
444 }
445 let slice = std::slice::from_raw_parts(content, len);
446 result.push_str(&String::from_utf8_lossy(slice));
447 }
448 }
449 child = (*child).next;
450 }
451 }
452 result
453}
454
455unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
461 if node.is_null() {
462 return None;
463 }
464 unsafe {
465 let mut prop = (*node).properties;
466 while !prop.is_null() {
467 let prop_name = (*prop).name;
468 if !prop_name.is_null() {
469 let mut len = 0;
470 while *prop_name.add(len) != 0 {
471 len += 1;
472 }
473 let slice = std::slice::from_raw_parts(prop_name, len);
474 if let Ok(s) = std::str::from_utf8(slice) {
475 if s == name {
476 return Some(get_node_text(prop as *mut _xmlNode));
477 }
478 }
479 }
480 prop = (*prop).next;
481 }
482 }
483 None
484}
485
486unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
492 if node.is_null() {
493 return false;
494 }
495 unsafe {
496 let name = (*node).name;
497 if name.is_null() {
498 return false;
499 }
500 let mut len = 0;
501 while *name.add(len) != 0 {
502 len += 1;
503 }
504 let slice = std::slice::from_raw_parts(name, len);
505 if let Ok(s) = std::str::from_utf8(slice) {
506 let local = if let Some(pos) = s.find(':') {
507 &s[pos + 1..]
508 } else {
509 s
510 };
511 return local == local_name;
512 }
513 }
514 false
515}
516
517unsafe fn child_elements(node: *mut _xmlNode) -> Vec<*mut _xmlNode> {
523 let mut children = Vec::new();
524 if node.is_null() {
525 return children;
526 }
527 unsafe {
528 let mut child = (*node).children;
529 while !child.is_null() {
530 if (*child).type_ == XML_ELEMENT_NODE as c_int {
531 children.push(child);
532 }
533 child = (*child).next;
534 }
535 }
536 children
537}
538
539unsafe fn get_inline_text(node: *mut _xmlNode) -> String {
546 if node.is_null() {
547 return String::new();
548 }
549 let mut result = String::new();
550 unsafe {
551 let mut child = (*node).children;
552 while !child.is_null() {
553 if (*child).type_ == XML_TEXT_NODE as c_int
554 || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
555 {
556 if !(*child).content.is_null() {
557 let content = (*child).content;
558 let mut len = 0;
559 while *content.add(len) != 0 {
560 len += 1;
561 }
562 let slice = std::slice::from_raw_parts(content, len);
563 result.push_str(&String::from_utf8_lossy(slice));
564 }
565 } else if (*child).type_ == XML_ELEMENT_NODE as c_int {
566 let local = get_local_name(child);
567 match local.as_str() {
568 "span" | "emph" | "dir" => {
569 result.push_str(&get_inline_text(child));
570 }
571 _ => {}
572 }
573 }
574 child = (*child).next;
575 }
576 }
577 result
578}
579
580pub fn schematron_parse(xml_doc: &str) -> Result<SchematronSchema, String> {
593 let doc_ptr = unsafe {
594 crate::abi::exports_xml2::xmlReadMemory(
595 xml_doc.as_ptr() as *const c_char,
596 xml_doc.len() as c_int,
597 b"schema.sch\0".as_ptr() as *const c_char,
598 ptr::null(),
599 0,
600 )
601 };
602
603 if doc_ptr.is_null() {
604 return Err("Failed to parse Schematron schema XML document".to_string());
605 }
606
607 let result = unsafe { schematron_parse_doc(doc_ptr) };
608 unsafe {
609 crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
610 }
611 result
612}
613
614unsafe fn schematron_parse_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
620 unsafe {
621 let root = (*doc).children;
622 if root.is_null() {
623 return Err("Schematron document has no root element".to_string());
624 }
625
626 let mut root_elem = root;
628 while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
629 root_elem = (*root_elem).next;
630 }
631
632 if root_elem.is_null() {
633 return Err("Schematron document has no root element".to_string());
634 }
635
636 let local_name = get_local_name(root_elem);
637 if local_name != "schema" {
638 return Err(format!(
639 "Expected '<schema>' root element, found '<{}>'",
640 local_name
641 ));
642 }
643
644 Ok(schematron_parse_schema_node(root_elem))
645 }
646}
647
648unsafe fn schematron_parse_schema_node(node: *mut _xmlNode) -> SchematronSchema {
654 unsafe {
655 let mut schema = SchematronSchema::new();
656
657 if let Some(qb) = get_attr(node, "queryBinding") {
659 schema.query_binding = qb;
660 }
661 schema.title = get_attr(node, "title");
662 if let Some(df) = get_attr(node, "defaultPhase") {
663 schema.default_phase = Some(df);
664 }
665
666 let mut current_pattern_id: Option<String> = None;
668 let mut pattern_names: HashMap<String, Vec<String>> = HashMap::new();
669
670 let mut child = (*node).children;
671 while !child.is_null() {
672 if (*child).type_ == XML_ELEMENT_NODE as c_int {
673 let local = get_local_name(child);
674 match local.as_str() {
675 "title" => {
676 if schema.title.is_none() {
677 schema.title = Some(get_node_text(child).trim().to_string());
678 }
679 }
680 "ns" => {
681 let prefix = get_attr(child, "prefix").unwrap_or_default();
682 let uri = get_attr(child, "uri").unwrap_or_default();
683 if !prefix.is_empty() && !uri.is_empty() {
684 schema.ns.insert(prefix, uri);
685 }
686 }
687 "phase" => {
688 let phase = schematron_parse_phase(child);
689 schema.phases.insert(phase.id.clone(), phase);
690 }
691 "pattern" => {
692 let pat_id = schematron_parse_pattern_node(
693 child,
694 &mut schema,
695 &mut current_pattern_id,
696 &mut pattern_names,
697 );
698 current_pattern_id = pat_id;
699 }
700 "rule" => {
701 let rule = schematron_parse_rule(child, &mut schema);
703 let rule_id = rule
704 .id
705 .clone()
706 .unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
707 let rid = rule_id.clone();
709 schema.rules.insert(rid, rule);
710
711 if let Some(ref pid) = current_pattern_id {
713 schema
714 .pattern_groups
715 .entry(pid.clone())
716 .or_insert_with(Vec::new)
717 .push(rule_id);
718 } else {
719 let anon_id = format!("_anon_{}", schema.pattern_order.len());
721 schema
722 .pattern_groups
723 .entry(anon_id.clone())
724 .or_insert_with(Vec::new)
725 .push(rule_id);
726 if !schema.pattern_order.contains(&anon_id) {
727 schema.pattern_order.push(anon_id);
728 }
729 }
730 }
731 "diagnostics" => {
732 schematron_parse_diagnostics(child, &mut schema);
733 }
734 "include" => {
735 schematron_parse_include(child, &mut schema);
736 }
737 "p" | "caption" => {
738 }
740 _ => {
741 schema
742 .errors
743 .push(format!("Unexpected element '<{}>' in schema", local));
744 }
745 }
746 }
747 child = (*child).next;
748 }
749
750 schema
751 }
752}
753
754unsafe fn schematron_parse_pattern_node(
760 node: *mut _xmlNode,
761 schema: &mut SchematronSchema,
762 _current_pattern_id: &mut Option<String>,
763 _pattern_names: &mut HashMap<String, Vec<String>>,
764) -> Option<String> {
765 unsafe {
766 let pat_id = get_attr(node, "id");
767 let pat_name = get_attr(node, "name");
768 let pat_is_a = get_attr(node, "is-a");
769 let pat_see = get_attr(node, "see");
770 let pat_icon = get_attr(node, "icon");
771 let pat_role = get_attr(node, "role");
772
773 let pid = pat_id
774 .clone()
775 .unwrap_or_else(|| format!("_pattern_{}", schema.pattern_order.len()));
776
777 let mut rule_ids: Vec<String> = Vec::new();
778
779 let mut child = (*node).children;
781 while !child.is_null() {
782 if (*child).type_ == XML_ELEMENT_NODE as c_int {
783 let local = get_local_name(child);
784 match local.as_str() {
785 "rule" => {
786 let rule = schematron_parse_rule(child, schema);
787 let rule_id = rule
788 .id
789 .clone()
790 .unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
791 let rid = rule_id.clone();
792 schema.rules.insert(rid, rule);
793 rule_ids.push(rule_id);
794 }
795 "p" | "caption" => {
796 }
798 _ => {
799 schema
800 .errors
801 .push(format!("Unexpected element '<{}>' in pattern", local));
802 }
803 }
804 }
805 child = (*child).next;
806 }
807
808 schema.pattern_groups.insert(pid.clone(), rule_ids);
809 schema.pattern_order.push(pid.clone());
810
811 if pat_is_a.is_some() {
813 }
816
817 let _ = pat_name;
819 let _ = pat_see;
820 let _ = pat_icon;
821 let _ = pat_role;
822
823 Some(pid)
824 }
825}
826
827unsafe fn schematron_parse_rule(
833 node: *mut _xmlNode,
834 schema: &mut SchematronSchema,
835) -> SchematronRule {
836 unsafe {
837 let context = get_attr(node, "context").unwrap_or_default();
838 let mut rule = SchematronRule::new(context);
839 rule.id = get_attr(node, "id");
840
841 let abs = get_attr(node, "abstract").unwrap_or_default();
842 rule.abstract_ = abs == "true" || abs == "1";
843
844 let mut child = (*node).children;
846 while !child.is_null() {
847 if (*child).type_ == XML_ELEMENT_NODE as c_int {
848 let local = get_local_name(child);
849 match local.as_str() {
850 "assert" => {
851 let pattern = schematron_parse_assert(child, SchematronPatternType::Assert);
852 rule.patterns.push(pattern);
853 }
854 "report" => {
855 let pattern = schematron_parse_assert(child, SchematronPatternType::Report);
856 rule.patterns.push(pattern);
857 }
858 "extends" => {
859 if let Some(ext_rule) = get_attr(child, "rule") {
860 rule.extends.push(ext_rule);
861 }
862 }
863 "let" => {
864 let name = get_attr(child, "name").unwrap_or_default();
867 let value = get_attr(child, "value").unwrap_or_default();
868 if !name.is_empty() {
869 let _ = value;
872 }
873 }
874 "param" => {
875 let _name = get_attr(child, "name");
877 let _value = get_attr(child, "value");
878 }
879 "p" | "caption" => {
880 }
882 _ => {
883 schema
884 .errors
885 .push(format!("Unexpected element '<{}>' in rule", local));
886 }
887 }
888 }
889 child = (*child).next;
890 }
891
892 rule
893 }
894}
895
896unsafe fn schematron_parse_assert(
902 node: *mut _xmlNode,
903 pattern_type: SchematronPatternType,
904) -> SchematronPattern {
905 unsafe {
906 let test = get_attr(node, "test").unwrap_or_default();
907 let text = get_inline_text(node);
908
909 let mut pattern = SchematronPattern::new(pattern_type, test, text);
910 pattern.flag = get_attr(node, "flag");
911 pattern.id = get_attr(node, "id");
912 pattern.icon = get_attr(node, "icon");
913 pattern.see = get_attr(node, "see");
914 pattern.role = get_attr(node, "role");
915 pattern.diagnostics = get_attr(node, "diagnostics");
916
917 pattern
921 }
922}
923
924unsafe fn schematron_parse_phase(node: *mut _xmlNode) -> SchematronPhase {
930 unsafe {
931 let id = get_attr(node, "id").unwrap_or_default();
932 let mut phase = SchematronPhase {
933 id,
934 active_patterns: Vec::new(),
935 };
936
937 let mut child = (*node).children;
938 while !child.is_null() {
939 if (*child).type_ == XML_ELEMENT_NODE as c_int {
940 let local = get_local_name(child);
941 if local == "active" {
942 if let Some(pattern) = get_attr(child, "pattern") {
943 phase.active_patterns.push(pattern);
944 }
945 }
946 }
947 child = (*child).next;
948 }
949
950 phase
951 }
952}
953
954unsafe fn schematron_parse_diagnostics(node: *mut _xmlNode, schema: &mut SchematronSchema) {
960 unsafe {
961 let mut child = (*node).children;
962 while !child.is_null() {
963 if (*child).type_ == XML_ELEMENT_NODE as c_int {
964 let local = get_local_name(child);
965 if local == "diagnostic" {
966 let diag = schematron_parse_diagnostic(child);
967 schema.diagnostics.insert(diag.id.clone(), diag);
968 }
969 }
970 child = (*child).next;
971 }
972 }
973}
974
975unsafe fn schematron_parse_diagnostic(node: *mut _xmlNode) -> SchematronDiagnostic {
981 unsafe {
982 let id = get_attr(node, "id").unwrap_or_default();
983 let text = get_inline_text(node);
984 let icon = get_attr(node, "icon");
985 let see = get_attr(node, "see");
986
987 SchematronDiagnostic {
988 id,
989 text,
990 icon,
991 see,
992 }
993 }
994}
995
996unsafe fn schematron_parse_include(node: *mut _xmlNode, _schema: &mut SchematronSchema) {
1002 unsafe {
1003 let href = get_attr(node, "href");
1004 if let Some(url) = href {
1005 let url_c = std::ffi::CString::new(url.clone()).ok();
1006 if let Some(c) = url_c {
1007 let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1008 if !doc.is_null() {
1009 let mut root = (*doc).children;
1011 while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
1012 root = (*root).next;
1013 }
1014 if !root.is_null() {
1015 let local = get_local_name(root);
1016 if local == "schema" || local == "pattern" || local == "rule" {
1017 }
1021 }
1022 crate::abi::exports_xml2::xmlFreeDoc(doc);
1023 }
1024 }
1025 }
1026 }
1027}
1028
1029unsafe fn expand_diagnostic_message(
1042 text: &str,
1043 context_node: *mut _xmlNode,
1044 xpath_ctxt: &mut XPathContext,
1045) -> String {
1046 let _ = context_node;
1058 let _ = xpath_ctxt;
1059 text.to_string()
1060}
1061
1062fn evaluate_xpath_boolean(
1068 compiled: &CompiledExpr,
1069 xpath_ctxt: &mut XPathContext,
1070) -> Result<bool, String> {
1071 match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1072 Some(value) => Ok(value.as_boolean()),
1073 None => Err("XPath evaluation failed".to_string()),
1074 }
1075}
1076
1077pub unsafe fn schematron_validate_doc(
1087 schema: &SchematronSchema,
1088 doc: *mut _xmlDoc,
1089 ctxt: &mut SchematronValidCtxt,
1090) -> bool {
1091 unsafe {
1092 if doc.is_null() {
1093 ctxt.record_error("Document is null".to_string());
1094 return false;
1095 }
1096
1097 let root = (*doc).children;
1098 if root.is_null() {
1099 ctxt.record_error("Document has no children".to_string());
1100 return false;
1101 }
1102
1103 let mut root_elem = root;
1105 while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
1106 root_elem = (*root_elem).next;
1107 }
1108
1109 if root_elem.is_null() {
1110 ctxt.record_error("Document has no root element".to_string());
1111 return false;
1112 }
1113
1114 let phase_id = ctxt.active_phase.as_deref();
1116 let rules = schema.active_rules(phase_id);
1117
1118 if rules.is_empty() {
1119 return true;
1121 }
1122
1123 let mut xpath_ctxt = XPathContext::new(doc);
1125
1126 let core_funcs = crate::xml::xpath::functions::core_functions();
1128 for (name, func) in core_funcs {
1129 xpath_ctxt.register_function(&name, func);
1130 }
1131
1132 for (prefix, uri) in &schema.ns {
1134 xpath_ctxt.namespaces.insert(prefix.clone(), uri.clone());
1135 }
1136
1137 let mut valid = true;
1138
1139 for rule in &rules {
1141 let matching_nodes: Vec<*mut _xmlNode> =
1143 find_matching_nodes(rule, root_elem, doc, &mut xpath_ctxt);
1144
1145 for context_node in &matching_nodes {
1146 xpath_ctxt.set_context_node(*context_node);
1148
1149 for pattern in &rule.patterns {
1151 let compiled = match &pattern.compiled_test {
1152 Some(c) => c,
1153 None => continue,
1154 };
1155
1156 let test_result = match evaluate_xpath_boolean(compiled, &mut xpath_ctxt) {
1157 Ok(val) => val,
1158 Err(e) => {
1159 ctxt.record_error(format!(
1160 "XPath error in '{}' test '{}': {}",
1161 if pattern.pattern_type == SchematronPatternType::Assert {
1162 "assert"
1163 } else {
1164 "report"
1165 },
1166 pattern.test,
1167 e
1168 ));
1169 valid = false;
1170 continue;
1171 }
1172 };
1173
1174 let message =
1175 expand_diagnostic_message(&pattern.text, *context_node, &mut xpath_ctxt);
1176
1177 match pattern.pattern_type {
1178 SchematronPatternType::Assert => {
1179 if !test_result {
1181 let node_name = get_node_qname(*context_node);
1182 let flag_str = pattern
1183 .flag
1184 .as_ref()
1185 .map(|f| format!(" [{}]", f))
1186 .unwrap_or_default();
1187 let role_str = pattern
1188 .role
1189 .as_ref()
1190 .map(|r| format!(" ({})", r))
1191 .unwrap_or_default();
1192 let msg = if message.is_empty() {
1193 format!(
1194 "assertion failed: '{}' for node '{}'{}{}",
1195 pattern.test, node_name, flag_str, role_str
1196 )
1197 } else {
1198 format!(
1199 "assertion '{}' failed for node '{}'{}{}: {}",
1200 pattern.test, node_name, flag_str, role_str, message
1201 )
1202 };
1203 ctxt.record_error(msg);
1204 valid = false;
1205 }
1206 }
1207 SchematronPatternType::Report => {
1208 if test_result {
1210 let node_name = get_node_qname(*context_node);
1211 let flag_str = pattern
1212 .flag
1213 .as_ref()
1214 .map(|f| format!(" [{}]", f))
1215 .unwrap_or_default();
1216 let role_str = pattern
1217 .role
1218 .as_ref()
1219 .map(|r| format!(" ({})", r))
1220 .unwrap_or_default();
1221 let msg = if message.is_empty() {
1222 format!(
1223 "report triggered: '{}' for node '{}'{}{}",
1224 pattern.test, node_name, flag_str, role_str
1225 )
1226 } else {
1227 format!(
1228 "report '{}' triggered for node '{}'{}{}: {}",
1229 pattern.test, node_name, flag_str, role_str, message
1230 )
1231 };
1232 ctxt.record_error(msg);
1233 valid = false;
1234 }
1235 }
1236 }
1237 }
1238 }
1239 }
1240
1241 valid
1242 }
1243}
1244
1245unsafe fn find_matching_nodes(
1252 rule: &SchematronRule,
1253 root: *mut _xmlNode,
1254 doc: *mut _xmlDoc,
1255 xpath_ctxt: &mut XPathContext,
1256) -> Vec<*mut _xmlNode> {
1257 unsafe {
1258 if rule.context.is_empty() {
1260 let mut nodes = Vec::new();
1261 collect_all_elements(root, &mut nodes);
1262 return nodes;
1263 }
1264
1265 if let Some(compiled) = &rule.compiled_context {
1267 let is_simple_name = !rule.context.contains('/')
1270 && !rule.context.contains("::")
1271 && !rule.context.contains('[')
1272 && !rule.context.contains('(');
1273
1274 if !is_simple_name {
1275 xpath_ctxt.set_context_node(root);
1276 xpath_ctxt.document = doc;
1277
1278 match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1279 Some(XPathValue::NodeSet(ns)) => {
1280 if !ns.is_empty() {
1281 return ns.iter().collect();
1282 }
1283 }
1284 _ => {}
1285 }
1286 }
1287
1288 simple_context_match(&rule.context, root)
1290 } else {
1291 simple_context_match(&rule.context, root)
1293 }
1294 }
1295}
1296
1297fn simple_context_match(context: &str, root: *mut _xmlNode) -> Vec<*mut _xmlNode> {
1300 unsafe {
1301 let context = context.trim();
1302
1303 if context == "*" || context == "//*" {
1310 let mut nodes = Vec::new();
1311 collect_all_elements(root, &mut nodes);
1312 return nodes;
1313 }
1314
1315 if context.starts_with("//") {
1316 let name = &context[2..];
1317 if name.is_empty() || name == "*" {
1318 let mut nodes = Vec::new();
1319 collect_all_elements(root, &mut nodes);
1320 return nodes;
1321 }
1322 let mut nodes = Vec::new();
1324 collect_elements_by_name(root, name, &mut nodes);
1325 return nodes;
1326 }
1327
1328 if !context.contains('/') && !context.contains("::") {
1329 let mut nodes = Vec::new();
1331 let root_qname = get_node_qname(root);
1333 let root_local = get_local_name(root);
1334 if root_qname == context || root_local == context || context == "*" {
1335 nodes.push(root);
1336 }
1337 let mut child = (*root).children;
1339 while !child.is_null() {
1340 if (*child).type_ == XML_ELEMENT_NODE as c_int {
1341 let qname = get_node_qname(child);
1342 let local = get_local_name(child);
1343 if qname == context || local == context || context == "*" {
1344 nodes.push(child);
1345 }
1346 }
1347 child = (*child).next;
1348 }
1349 return nodes;
1350 }
1351
1352 vec![root]
1354 }
1355}
1356
1357unsafe fn collect_all_elements(node: *mut _xmlNode, nodes: &mut Vec<*mut _xmlNode>) {
1363 unsafe {
1364 if node.is_null() {
1365 return;
1366 }
1367 if (*node).type_ == XML_ELEMENT_NODE as c_int {
1368 nodes.push(node);
1369 }
1370 let mut child = (*node).children;
1371 while !child.is_null() {
1372 collect_all_elements(child, nodes);
1373 child = (*child).next;
1374 }
1375 }
1376}
1377
1378unsafe fn collect_elements_by_name(
1384 node: *mut _xmlNode,
1385 name: &str,
1386 nodes: &mut Vec<*mut _xmlNode>,
1387) {
1388 unsafe {
1389 if node.is_null() {
1390 return;
1391 }
1392 if (*node).type_ == XML_ELEMENT_NODE as c_int {
1393 let qname = get_node_qname(node);
1394 let local = get_local_name(node);
1395 if qname == name || local == name {
1396 nodes.push(node);
1397 }
1398 }
1399 let mut child = (*node).children;
1400 while !child.is_null() {
1401 collect_elements_by_name(child, name, nodes);
1402 child = (*child).next;
1403 }
1404 }
1405}
1406
1407pub fn schematron_parse_schema(xml_doc: &str) -> Result<SchematronSchema, String> {
1415 schematron_parse(xml_doc)
1416}
1417
1418pub unsafe fn schematron_parse_schema_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
1424 schematron_parse_doc(doc)
1425}
1426
1427pub unsafe fn schematron_validate_doc_schema(
1435 schema: &SchematronSchema,
1436 doc: *mut _xmlDoc,
1437 ctxt: &mut SchematronValidCtxt,
1438) -> bool {
1439 schematron_validate_doc(schema, doc, ctxt)
1440}
1441
1442#[no_mangle]
1461pub unsafe extern "C" fn xmlSchematronNewParserCtxt(url: *const c_char) -> *mut c_void {
1462 if url.is_null() {
1463 return Box::into_raw(Box::new(SchematronSchema::new())) as *mut c_void;
1469 }
1470
1471 let url_str = unsafe {
1472 let mut len = 0;
1473 while *url.add(len) != 0 {
1474 len += 1;
1475 }
1476 let slice = std::slice::from_raw_parts(url as *const u8, len);
1477 String::from_utf8_lossy(slice).to_string()
1478 };
1479
1480 if !url_str.is_empty() {
1482 let url_c = std::ffi::CString::new(url_str.clone()).ok();
1483 if let Some(c) = url_c {
1484 let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1485 if !doc.is_null() {
1486 let result = schematron_parse_doc(doc);
1487 crate::abi::exports_xml2::xmlFreeDoc(doc);
1488 if let Ok(schema) = result {
1489 let schema_box = Box::new(schema);
1490 return Box::into_raw(schema_box) as *mut c_void;
1491 }
1492 }
1493 }
1494 }
1495
1496 Box::into_raw(Box::new(SchematronSchema::new())) as *mut c_void
1498}
1499
1500#[no_mangle]
1512pub unsafe extern "C" fn xmlSchematronNewMemParserCtxt(
1513 buffer: *const c_char,
1514 size: c_int,
1515) -> *mut c_void {
1516 if buffer.is_null() || size <= 0 {
1517 return ptr::null_mut();
1518 }
1519
1520 let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
1522 let xml_str = String::from_utf8_lossy(buf_slice).to_string();
1523
1524 match schematron_parse(&xml_str) {
1525 Ok(schema) => {
1526 let schema_box = Box::new(schema);
1527 Box::into_raw(schema_box) as *mut c_void
1528 }
1529 Err(_) => ptr::null_mut(),
1530 }
1531}
1532
1533#[no_mangle]
1545pub unsafe extern "C" fn xmlSchematronParse(ctxt: *mut c_void) -> *mut c_void {
1546 if ctxt.is_null() {
1547 return ptr::null_mut();
1548 }
1549
1550 ctxt
1553}
1554
1555#[no_mangle]
1567pub unsafe extern "C" fn xmlSchematronFree(schema: *mut c_void) {
1568 if schema.is_null() {
1569 return;
1570 }
1571 unsafe {
1573 let _ = Box::from_raw(schema as *mut SchematronSchema);
1574 }
1575}
1576
1577#[no_mangle]
1589pub unsafe extern "C" fn xmlSchematronFreeParserCtxt(ctxt: *mut c_void) {
1590 if ctxt.is_null() {
1591 return;
1592 }
1593 unsafe {
1595 let _ = Box::from_raw(ctxt as *mut SchematronSchema);
1596 }
1597}
1598
1599#[no_mangle]
1611pub unsafe extern "C" fn xmlSchematronNewValidCtxt(schema: *mut c_void) -> *mut c_void {
1612 let mut ctxt = SchematronValidCtxt::new();
1613
1614 if !schema.is_null() {
1615 unsafe {
1617 let schema_ref = &*(schema as *const SchematronSchema);
1618 ctxt.schema = Some(schema_ref.clone());
1619 }
1620 }
1621
1622 let boxed = Box::new(ctxt);
1623 Box::into_raw(boxed) as *mut c_void
1624}
1625
1626#[no_mangle]
1638pub unsafe extern "C" fn xmlSchematronFreeValidCtxt(ctxt: *mut c_void) {
1639 if ctxt.is_null() {
1640 return;
1641 }
1642 unsafe {
1644 let _ = Box::from_raw(ctxt as *mut SchematronValidCtxt);
1645 }
1646}
1647
1648#[no_mangle]
1663pub unsafe extern "C" fn xmlSchematronValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
1664 if ctxt.is_null() || doc.is_null() {
1665 return -1;
1666 }
1667
1668 unsafe {
1669 let valid_ctxt = &mut *(ctxt as *mut SchematronValidCtxt);
1670 let schema = match &valid_ctxt.schema {
1671 Some(s) => s,
1672 None => return -1,
1673 };
1674
1675 let mut temp_ctxt = SchematronValidCtxt::new();
1676 temp_ctxt.active_phase = valid_ctxt.active_phase.clone();
1677
1678 let valid = schematron_validate_doc(schema, doc, &mut temp_ctxt);
1679
1680 if valid {
1681 0
1682 } else {
1683 valid_ctxt.errors = temp_ctxt.errors;
1684 valid_ctxt.nb_errors = temp_ctxt.nb_errors;
1685 temp_ctxt.nb_errors
1686 }
1687 }
1688}
1689
1690pub type SchematronValidityErrorFunc = unsafe extern "C" fn(ctx: *mut c_void, msg: *const c_char);
1703
1704pub type SchematronValidityWarningFunc = unsafe extern "C" fn(ctx: *mut c_void, msg: *const c_char);
1706
1707struct SchematronSendPtr(*mut c_void);
1708unsafe impl Send for SchematronSendPtr {}
1709unsafe impl Sync for SchematronSendPtr {}
1710impl Clone for SchematronSendPtr {
1711 fn clone(&self) -> Self {
1712 SchematronSendPtr(self.0)
1713 }
1714}
1715impl Copy for SchematronSendPtr {}
1716impl Default for SchematronSendPtr {
1717 fn default() -> Self {
1718 SchematronSendPtr(core::ptr::null_mut())
1719 }
1720}
1721
1722#[derive(Clone, Copy, Default)]
1723struct SchematronParserState {
1724 err: Option<SchematronValidityErrorFunc>,
1725 warn: Option<SchematronValidityWarningFunc>,
1726 ctx: SchematronSendPtr,
1727}
1728
1729#[derive(Clone, Copy, Default)]
1730struct SchematronValidState {
1731 err: Option<SchematronValidityErrorFunc>,
1732 warn: Option<SchematronValidityWarningFunc>,
1733 ctx: SchematronSendPtr,
1734 options: c_int,
1735}
1736
1737static SCHEMATRON_PARSER_STATE: once_cell::sync::Lazy<
1738 parking_lot::Mutex<std::collections::HashMap<usize, SchematronParserState>>,
1739> = once_cell::sync::Lazy::new(Default::default);
1740
1741static SCHEMATRON_VALID_STATE: once_cell::sync::Lazy<
1742 parking_lot::Mutex<std::collections::HashMap<usize, SchematronValidState>>,
1743> = once_cell::sync::Lazy::new(Default::default);
1744
1745#[no_mangle]
1748pub unsafe extern "C" fn xmlSchematronSetParserErrors(
1749 ctxt: *mut c_void,
1750 err: Option<SchematronValidityErrorFunc>,
1751 warn: Option<SchematronValidityWarningFunc>,
1752 ctx: *mut c_void,
1753) {
1754 if ctxt.is_null() {
1755 return;
1756 }
1757 let mut map = SCHEMATRON_PARSER_STATE.lock();
1758 let st = map.entry(ctxt as usize).or_default();
1759 st.err = err;
1760 st.warn = warn;
1761 st.ctx = SchematronSendPtr(ctx);
1762}
1763
1764#[no_mangle]
1766pub unsafe extern "C" fn xmlSchematronGetParserErrors(
1767 ctxt: *mut c_void,
1768 err: *mut Option<SchematronValidityErrorFunc>,
1769 warn: *mut Option<SchematronValidityWarningFunc>,
1770 ctx: *mut *mut c_void,
1771) -> c_int {
1772 if ctxt.is_null() {
1773 return -1;
1774 }
1775 let map = SCHEMATRON_PARSER_STATE.lock();
1776 let st = map.get(&(ctxt as usize)).copied().unwrap_or_default();
1777 if !err.is_null() {
1778 *err = st.err;
1779 }
1780 if !warn.is_null() {
1781 *warn = st.warn;
1782 }
1783 if !ctx.is_null() {
1784 *ctx = st.ctx.0;
1785 }
1786 0
1787}
1788
1789#[no_mangle]
1791pub unsafe extern "C" fn xmlSchematronSetValidErrors(
1792 ctxt: *mut c_void,
1793 err: Option<SchematronValidityErrorFunc>,
1794 warn: Option<SchematronValidityWarningFunc>,
1795 ctx: *mut c_void,
1796) {
1797 if ctxt.is_null() {
1798 return;
1799 }
1800 let mut map = SCHEMATRON_VALID_STATE.lock();
1801 let st = map.entry(ctxt as usize).or_default();
1802 st.err = err;
1803 st.warn = warn;
1804 st.ctx = SchematronSendPtr(ctx);
1805}
1806
1807#[no_mangle]
1809pub unsafe extern "C" fn xmlSchematronGetValidErrors(
1810 ctxt: *mut c_void,
1811 err: *mut Option<SchematronValidityErrorFunc>,
1812 warn: *mut Option<SchematronValidityWarningFunc>,
1813 ctx: *mut *mut c_void,
1814) -> c_int {
1815 if ctxt.is_null() {
1816 return -1;
1817 }
1818 let map = SCHEMATRON_VALID_STATE.lock();
1819 let st = map.get(&(ctxt as usize)).copied().unwrap_or_default();
1820 if !err.is_null() {
1821 *err = st.err;
1822 }
1823 if !warn.is_null() {
1824 *warn = st.warn;
1825 }
1826 if !ctx.is_null() {
1827 *ctx = st.ctx.0;
1828 }
1829 0
1830}
1831
1832#[no_mangle]
1835pub unsafe extern "C" fn xmlSchematronSetValidOptions(ctxt: *mut c_void, options: c_int) -> c_int {
1836 if ctxt.is_null() {
1837 return -1;
1838 }
1839 let mut map = SCHEMATRON_VALID_STATE.lock();
1840 let st = map.entry(ctxt as usize).or_default();
1841 let old = st.options;
1842 st.options = options;
1843 old
1844}
1845
1846#[no_mangle]
1848pub unsafe extern "C" fn xmlSchematronValidCtxtGetOptions(ctxt: *mut c_void) -> c_int {
1849 if ctxt.is_null() {
1850 return -1;
1851 }
1852 SCHEMATRON_VALID_STATE
1853 .lock()
1854 .get(&(ctxt as usize))
1855 .map_or(0, |st| st.options)
1856}
1857
1858#[no_mangle]
1861pub unsafe extern "C" fn xmlSchematronIsValid(ctxt: *mut c_void) -> c_int {
1862 if ctxt.is_null() {
1863 return 0;
1864 }
1865 unsafe {
1866 let vc = &*(ctxt as *const SchematronValidCtxt);
1867 if vc.nb_errors > 0 {
1868 0
1869 } else {
1870 1
1871 }
1872 }
1873}
1874
1875#[no_mangle]
1878pub unsafe extern "C" fn xmlSchematronValidateOneElement(
1879 ctxt: *mut c_void,
1880 elem: *mut _xmlNode,
1881) -> c_int {
1882 if ctxt.is_null() || elem.is_null() {
1883 return -1;
1884 }
1885 unsafe {
1886 let valid_ctxt = &mut *(ctxt as *mut SchematronValidCtxt);
1887 let schema = match &valid_ctxt.schema {
1888 Some(s) => s,
1889 None => return -1,
1890 };
1891 let doc = (*elem).doc;
1892 if doc.is_null() {
1893 return -1;
1894 }
1895 let mut temp_ctxt = SchematronValidCtxt::new();
1898 temp_ctxt.active_phase = valid_ctxt.active_phase.clone();
1899 let valid = schematron_validate_doc(schema, doc, &mut temp_ctxt);
1900 if !valid {
1901 valid_ctxt.errors = temp_ctxt.errors;
1902 valid_ctxt.nb_errors = temp_ctxt.nb_errors;
1903 }
1904 if valid {
1905 0
1906 } else {
1907 -1
1908 }
1909 }
1910}
1911
1912#[cfg(test)]
1917mod tests {
1918 use super::*;
1919
1920 #[test]
1923 fn test_parse_simple_schema() {
1924 let schema_xml = r#"<?xml version="1.0"?>
1925<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1926 <pattern id="P1">
1927 <rule context="root">
1928 <assert test="count(*) > 0">Root must have children</assert>
1929 </rule>
1930 </pattern>
1931</schema>"#;
1932
1933 let result = schematron_parse(schema_xml);
1934 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1935 let schema = result.unwrap();
1936 assert_eq!(schema.pattern_order.len(), 1);
1937 assert_eq!(schema.rules.len(), 1);
1938 }
1939
1940 #[test]
1941 fn test_parse_with_ns() {
1942 let schema_xml = r#"<?xml version="1.0"?>
1943<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1944 <ns prefix="doc" uri="http://example.com/doc"/>
1945 <pattern id="P1">
1946 <rule context="doc:entry">
1947 <assert test="doc:title">Entry must have a title</assert>
1948 </rule>
1949 </pattern>
1950</schema>"#;
1951
1952 let result = schematron_parse(schema_xml);
1953 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1954 let schema = result.unwrap();
1955 assert!(schema.ns.contains_key("doc"));
1956 assert_eq!(schema.ns.get("doc").unwrap(), "http://example.com/doc");
1957 }
1958
1959 #[test]
1960 fn test_parse_with_phases() {
1961 let schema_xml = r#"<?xml version="1.0"?>
1962<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
1963 <phase id="phaseA">
1964 <active pattern="P1"/>
1965 </phase>
1966 <phase id="phaseB">
1967 <active pattern="P2"/>
1968 </phase>
1969 <pattern id="P1">
1970 <rule context="root">
1971 <assert test="true()">Always passes</assert>
1972 </rule>
1973 </pattern>
1974 <pattern id="P2">
1975 <rule context="root">
1976 <assert test="false()">Always fails</assert>
1977 </rule>
1978 </pattern>
1979</schema>"#;
1980
1981 let result = schematron_parse(schema_xml);
1982 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1983 let schema = result.unwrap();
1984 assert_eq!(schema.phases.len(), 2);
1985 assert!(schema.phases.contains_key("phaseA"));
1986 assert!(schema.phases.contains_key("phaseB"));
1987 assert_eq!(schema.default_phase.as_deref(), Some("phaseA"));
1988 }
1989
1990 #[test]
1991 fn test_parse_report_pattern() {
1992 let schema_xml = r#"<?xml version="1.0"?>
1993<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1994 <pattern id="P1">
1995 <rule context="root">
1996 <report test="@deprecated">Element is deprecated</report>
1997 </rule>
1998 </pattern>
1999</schema>"#;
2000
2001 let result = schematron_parse(schema_xml);
2002 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
2003 let schema = result.unwrap();
2004 let rule = schema.rules.values().next().unwrap();
2005 assert_eq!(rule.patterns.len(), 1);
2006 assert_eq!(rule.patterns[0].pattern_type, SchematronPatternType::Report);
2007 assert_eq!(rule.patterns[0].test, "@deprecated");
2008 }
2009
2010 #[test]
2011 fn test_parse_abstract_rule() {
2012 let schema_xml = r#"<?xml version="1.0"?>
2013<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2014 <pattern id="P1">
2015 <rule id="abstractRule" abstract="true" context="*">
2016 <assert test="true()">Abstract assertion</assert>
2017 </rule>
2018 <rule id="concreteRule" context="root">
2019 <extends rule="abstractRule"/>
2020 <assert test="count(*) > 0">Concrete assertion</assert>
2021 </rule>
2022 </pattern>
2023</schema>"#;
2024
2025 let result = schematron_parse(schema_xml);
2026 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
2027 let schema = result.unwrap();
2028 assert!(schema.rules.contains_key("abstractRule"));
2029 assert!(schema.rules.contains_key("concreteRule"));
2030 let abstract_rule = &schema.rules["abstractRule"];
2031 assert!(abstract_rule.abstract_);
2032 let concrete_rule = &schema.rules["concreteRule"];
2033 assert!(!concrete_rule.abstract_);
2034 assert_eq!(concrete_rule.extends.len(), 1);
2035 assert_eq!(concrete_rule.extends[0], "abstractRule");
2036 }
2037
2038 #[test]
2039 fn test_parse_with_diagnostics() {
2040 let schema_xml = r#"<?xml version="1.0"?>
2041<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2042 <diagnostics>
2043 <diagnostic id="diag1">This is a diagnostic message</diagnostic>
2044 </diagnostics>
2045 <pattern id="P1">
2046 <rule context="root">
2047 <assert test="true()" diagnostics="diag1">Assertion with diagnostic</assert>
2048 </rule>
2049 </pattern>
2050</schema>"#;
2051
2052 let result = schematron_parse(schema_xml);
2053 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
2054 let schema = result.unwrap();
2055 assert!(schema.diagnostics.contains_key("diag1"));
2056 assert_eq!(
2057 schema.diagnostics["diag1"].text,
2058 "This is a diagnostic message"
2059 );
2060 }
2061
2062 #[test]
2063 fn test_parse_with_attributes() {
2064 let schema_xml = r#"<?xml version="1.0"?>
2065<schema xmlns="http://purl.oclc.org/dsdl/schematron" title="Test Schema">
2066 <pattern id="P1">
2067 <rule context="root">
2068 <assert test="true()" flag="warn" role="error" id="a1" icon="info" see="http://example.com">
2069 Test message
2070 </assert>
2071 </rule>
2072 </pattern>
2073</schema>"#;
2074
2075 let result = schematron_parse(schema_xml);
2076 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
2077 let schema = result.unwrap();
2078 assert_eq!(schema.title.as_deref(), Some("Test Schema"));
2079 let rule = schema.rules.values().next().unwrap();
2080 let pat = &rule.patterns[0];
2081 assert_eq!(pat.flag.as_deref(), Some("warn"));
2082 assert_eq!(pat.role.as_deref(), Some("error"));
2083 assert_eq!(pat.id.as_deref(), Some("a1"));
2084 assert_eq!(pat.icon.as_deref(), Some("info"));
2085 assert_eq!(pat.see.as_deref(), Some("http://example.com"));
2086 }
2087
2088 #[test]
2089 fn test_parse_empty_schema() {
2090 let schema_xml = r#"<?xml version="1.0"?>
2091<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2092</schema>"#;
2093
2094 let result = schematron_parse(schema_xml);
2095 assert!(result.is_ok(), "Failed to parse empty schema");
2096 let schema = result.unwrap();
2097 assert!(schema.rules.is_empty());
2098 assert!(schema.phases.is_empty());
2099 }
2100
2101 #[test]
2102 fn test_parse_no_assertions() {
2103 let schema_xml = r#"<?xml version="1.0"?>
2104<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2105 <pattern id="P1">
2106 <rule context="root">
2107 </rule>
2108 </pattern>
2109</schema>"#;
2110
2111 let result = schematron_parse(schema_xml);
2112 assert!(result.is_ok(), "Failed to parse schema with no assertions");
2113 let schema = result.unwrap();
2114 let rule = schema.rules.values().next().unwrap();
2115 assert!(rule.patterns.is_empty());
2116 }
2117
2118 #[test]
2119 fn test_parse_invalid_root_element() {
2120 let schema_xml = r#"<?xml version="1.0"?>
2121<not-schema xmlns="http://purl.oclc.org/dsdl/schematron">
2122</not-schema>"#;
2123
2124 let result = schematron_parse(schema_xml);
2125 assert!(result.is_err(), "Should fail with wrong root element");
2126 assert!(
2127 result.err().unwrap().contains("Expected '<schema>'"),
2128 "Error should mention expected schema element"
2129 );
2130 }
2131
2132 #[test]
2133 fn test_parse_empty_document_fails() {
2134 let result = schematron_parse("");
2135 assert!(result.is_err());
2136 }
2137
2138 #[test]
2139 fn test_parse_invalid_xml_fails() {
2140 let result = schematron_parse("not valid xml <<<");
2141 assert!(result.is_err());
2142 }
2143
2144 #[test]
2145 fn test_parse_schema_with_let_and_param() {
2146 let schema_xml = r#"<?xml version="1.0"?>
2147<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2148 <pattern id="P1">
2149 <rule context="root">
2150 <let name="x" value="42"/>
2151 <param name="debug" value="true"/>
2152 <assert test="true()">Test with let and param</assert>
2153 </rule>
2154 </pattern>
2155</schema>"#;
2156
2157 let result = schematron_parse(schema_xml);
2158 assert!(
2159 result.is_ok(),
2160 "Failed to parse schema with let/param: {:?}",
2161 result.err()
2162 );
2163 let schema = result.unwrap();
2164 assert_eq!(schema.rules.len(), 1);
2165 }
2166
2167 #[test]
2168 fn test_parse_schema_with_documentation() {
2169 let schema_xml = r#"<?xml version="1.0"?>
2170<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2171 <p>This is documentation</p>
2172 <caption>Table caption</caption>
2173 <pattern id="P1">
2174 <p>Pattern documentation</p>
2175 <rule context="root">
2176 <p>Rule documentation</p>
2177 <assert test="true()">Real assertion</assert>
2178 </rule>
2179 </pattern>
2180</schema>"#;
2181
2182 let result = schematron_parse(schema_xml);
2183 assert!(result.is_ok(), "Failed to parse schema with documentation");
2184 let schema = result.unwrap();
2185 assert_eq!(schema.rules.len(), 1);
2186 }
2187
2188 #[test]
2191 fn test_validate_assert_pass() {
2192 let schema_xml = r#"<?xml version="1.0"?>
2193<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2194 <pattern id="P1">
2195 <rule context="root">
2196 <assert test="true()">Always passes</assert>
2197 </rule>
2198 </pattern>
2199</schema>"#;
2200
2201 let doc_xml = r#"<?xml version="1.0"?>
2202<root>Hello</root>"#;
2203
2204 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2205
2206 let doc = unsafe {
2207 crate::abi::exports_xml2::xmlReadMemory(
2208 doc_xml.as_ptr() as *const c_char,
2209 doc_xml.len() as c_int,
2210 b"test.xml\0".as_ptr() as *const c_char,
2211 ptr::null(),
2212 0,
2213 )
2214 };
2215 assert!(!doc.is_null(), "Failed to parse document");
2216
2217 let mut ctxt = SchematronValidCtxt::new();
2218 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2219 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2220
2221 assert!(valid, "Validation failed: {:?}", ctxt.errors);
2222 }
2223
2224 #[test]
2225 fn test_validate_assert_fail() {
2226 let schema_xml = r#"<?xml version="1.0"?>
2227<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2228 <pattern id="P1">
2229 <rule context="root">
2230 <assert test="false()">Always fails</assert>
2231 </rule>
2232 </pattern>
2233</schema>"#;
2234
2235 let doc_xml = r#"<?xml version="1.0"?>
2236<root>Hello</root>"#;
2237
2238 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2239
2240 let doc = unsafe {
2241 crate::abi::exports_xml2::xmlReadMemory(
2242 doc_xml.as_ptr() as *const c_char,
2243 doc_xml.len() as c_int,
2244 b"test.xml\0".as_ptr() as *const c_char,
2245 ptr::null(),
2246 0,
2247 )
2248 };
2249 assert!(!doc.is_null());
2250
2251 let mut ctxt = SchematronValidCtxt::new();
2252 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2253 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2254
2255 assert!(
2256 !valid,
2257 "Validation should have failed, errors: {:?}",
2258 ctxt.errors
2259 );
2260 assert!(ctxt.nb_errors > 0);
2261 }
2262
2263 #[test]
2264 fn test_validate_report_pass() {
2265 let schema_xml = r#"<?xml version="1.0"?>
2266<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2267 <pattern id="P1">
2268 <rule context="root">
2269 <report test="false()">Report should not trigger</report>
2270 </rule>
2271 </pattern>
2272</schema>"#;
2273
2274 let doc_xml = r#"<?xml version="1.0"?>
2275<root>Hello</root>"#;
2276
2277 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2278
2279 let doc = unsafe {
2280 crate::abi::exports_xml2::xmlReadMemory(
2281 doc_xml.as_ptr() as *const c_char,
2282 doc_xml.len() as c_int,
2283 b"test.xml\0".as_ptr() as *const c_char,
2284 ptr::null(),
2285 0,
2286 )
2287 };
2288 assert!(!doc.is_null());
2289
2290 let mut ctxt = SchematronValidCtxt::new();
2291 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2292 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2293
2294 assert!(valid, "Report should not trigger: {:?}", ctxt.errors);
2295 }
2296
2297 #[test]
2298 fn test_validate_report_fail() {
2299 let schema_xml = r#"<?xml version="1.0"?>
2300<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2301 <pattern id="P1">
2302 <rule context="root">
2303 <report test="true()">Report should trigger</report>
2304 </rule>
2305 </pattern>
2306</schema>"#;
2307
2308 let doc_xml = r#"<?xml version="1.0"?>
2309<root>Hello</root>"#;
2310
2311 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2312
2313 let doc = unsafe {
2314 crate::abi::exports_xml2::xmlReadMemory(
2315 doc_xml.as_ptr() as *const c_char,
2316 doc_xml.len() as c_int,
2317 b"test.xml\0".as_ptr() as *const c_char,
2318 ptr::null(),
2319 0,
2320 )
2321 };
2322 assert!(!doc.is_null());
2323
2324 let mut ctxt = SchematronValidCtxt::new();
2325 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2326 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2327
2328 assert!(!valid, "Report should have triggered");
2329 assert!(ctxt.nb_errors > 0);
2330 }
2331
2332 #[test]
2333 fn test_validate_context_matching() {
2334 let schema_xml = r#"<?xml version="1.0"?>
2335<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2336 <pattern id="P1">
2337 <rule context="child">
2338 <assert test="true()">Child matches</assert>
2339 </rule>
2340 </pattern>
2341</schema>"#;
2342
2343 let doc_xml = r#"<?xml version="1.0"?>
2344<root>
2345 <child>A</child>
2346 <child>B</child>
2347</root>"#;
2348
2349 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2350
2351 let doc = unsafe {
2352 crate::abi::exports_xml2::xmlReadMemory(
2353 doc_xml.as_ptr() as *const c_char,
2354 doc_xml.len() as c_int,
2355 b"test.xml\0".as_ptr() as *const c_char,
2356 ptr::null(),
2357 0,
2358 )
2359 };
2360 assert!(!doc.is_null());
2361
2362 let mut ctxt = SchematronValidCtxt::new();
2363 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2364 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2365
2366 assert!(valid, "Context matching failed: {:?}", ctxt.errors);
2367 }
2368
2369 #[test]
2370 fn test_validate_multiple_rules() {
2371 let schema_xml = r#"<?xml version="1.0"?>
2372<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2373 <pattern id="P1">
2374 <rule context="root">
2375 <assert test="true()">Root passes</assert>
2376 </rule>
2377 </pattern>
2378 <pattern id="P2">
2379 <rule context="child">
2380 <assert test="true()">Child passes</assert>
2381 </rule>
2382 </pattern>
2383</schema>"#;
2384
2385 let doc_xml = r#"<?xml version="1.0"?>
2386<root>
2387 <child>Content</child>
2388</root>"#;
2389
2390 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2391
2392 let doc = unsafe {
2393 crate::abi::exports_xml2::xmlReadMemory(
2394 doc_xml.as_ptr() as *const c_char,
2395 doc_xml.len() as c_int,
2396 b"test.xml\0".as_ptr() as *const c_char,
2397 ptr::null(),
2398 0,
2399 )
2400 };
2401 assert!(!doc.is_null());
2402
2403 let mut ctxt = SchematronValidCtxt::new();
2404 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2405 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2406
2407 assert!(valid, "Multiple rules failed: {:?}", ctxt.errors);
2408 }
2409
2410 #[test]
2411 fn test_validate_with_phase_filtering() {
2412 let schema_xml = r#"<?xml version="1.0"?>
2413<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
2414 <phase id="phaseA">
2415 <active pattern="P1"/>
2416 </phase>
2417 <phase id="phaseB">
2418 <active pattern="P2"/>
2419 </phase>
2420 <pattern id="P1">
2421 <rule context="root">
2422 <assert test="true()">Always passes</assert>
2423 </rule>
2424 </pattern>
2425 <pattern id="P2">
2426 <rule context="root">
2427 <assert test="false()">Always fails</assert>
2428 </rule>
2429 </pattern>
2430</schema>"#;
2431
2432 let doc_xml = r#"<?xml version="1.0"?>
2433<root>Hello</root>"#;
2434
2435 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2436
2437 let doc = unsafe {
2438 crate::abi::exports_xml2::xmlReadMemory(
2439 doc_xml.as_ptr() as *const c_char,
2440 doc_xml.len() as c_int,
2441 b"test.xml\0".as_ptr() as *const c_char,
2442 ptr::null(),
2443 0,
2444 )
2445 };
2446 assert!(!doc.is_null());
2447
2448 let mut ctxt = SchematronValidCtxt::new();
2450 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2451 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2452
2453 assert!(
2454 valid,
2455 "Phase filtering should make validation pass: {:?}",
2456 ctxt.errors
2457 );
2458 }
2459
2460 #[test]
2461 fn test_validate_no_rules() {
2462 let schema_xml = r#"<?xml version="1.0"?>
2463<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2464</schema>"#;
2465
2466 let doc_xml = r#"<?xml version="1.0"?>
2467<root>Hello</root>"#;
2468
2469 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2470
2471 let doc = unsafe {
2472 crate::abi::exports_xml2::xmlReadMemory(
2473 doc_xml.as_ptr() as *const c_char,
2474 doc_xml.len() as c_int,
2475 b"test.xml\0".as_ptr() as *const c_char,
2476 ptr::null(),
2477 0,
2478 )
2479 };
2480 assert!(!doc.is_null());
2481
2482 let mut ctxt = SchematronValidCtxt::new();
2483 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2484 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2485
2486 assert!(valid, "Empty schema should pass validation");
2487 }
2488
2489 #[test]
2490 fn test_validate_extends_resolution() {
2491 let schema_xml = r#"<?xml version="1.0"?>
2492<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2493 <pattern id="P1">
2494 <rule id="base" abstract="true" context="*">
2495 <assert test="true()">Base assertion</assert>
2496 </rule>
2497 <rule id="derived" context="root">
2498 <extends rule="base"/>
2499 <assert test="true()">Derived assertion</assert>
2500 </rule>
2501 </pattern>
2502</schema>"#;
2503
2504 let doc_xml = r#"<?xml version="1.0"?>
2505<root>Hello</root>"#;
2506
2507 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2508
2509 let resolved = schema.resolve_rule("derived");
2511 assert!(resolved.is_some());
2512 let resolved = resolved.unwrap();
2513 assert_eq!(
2515 resolved.patterns.len(),
2516 2,
2517 "Should have inherited the base pattern"
2518 );
2519
2520 let doc = unsafe {
2521 crate::abi::exports_xml2::xmlReadMemory(
2522 doc_xml.as_ptr() as *const c_char,
2523 doc_xml.len() as c_int,
2524 b"test.xml\0".as_ptr() as *const c_char,
2525 ptr::null(),
2526 0,
2527 )
2528 };
2529 assert!(!doc.is_null());
2530
2531 let mut ctxt = SchematronValidCtxt::new();
2532 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2533 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2534
2535 assert!(valid, "Extends resolution failed: {:?}", ctxt.errors);
2536 }
2537
2538 #[test]
2539 fn test_validate_assert_with_flag() {
2540 let schema_xml = r#"<?xml version="1.0"?>
2541<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2542 <pattern id="P1">
2543 <rule context="root">
2544 <assert test="false()" flag="warn">Warning message</assert>
2545 </rule>
2546 </pattern>
2547</schema>"#;
2548
2549 let doc_xml = r#"<?xml version="1.0"?>
2550<root>Hello</root>"#;
2551
2552 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2553
2554 let doc = unsafe {
2555 crate::abi::exports_xml2::xmlReadMemory(
2556 doc_xml.as_ptr() as *const c_char,
2557 doc_xml.len() as c_int,
2558 b"test.xml\0".as_ptr() as *const c_char,
2559 ptr::null(),
2560 0,
2561 )
2562 };
2563 assert!(!doc.is_null());
2564
2565 let mut ctxt = SchematronValidCtxt::new();
2566 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2567 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2568
2569 assert!(!valid);
2570 assert!(ctxt.nb_errors > 0);
2571 assert!(
2573 ctxt.errors[0].contains("[warn]"),
2574 "Error should include flag"
2575 );
2576 }
2577
2578 #[test]
2581 fn test_c_abi_new_free_parser_ctxt() {
2582 let ctxt = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2583 assert!(!ctxt.is_null());
2584 unsafe { xmlSchematronFreeParserCtxt(ctxt) };
2585 }
2587
2588 #[test]
2589 fn test_c_abi_new_free_valid_ctxt() {
2590 let schema = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2591 assert!(!schema.is_null());
2592
2593 let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2594 assert!(!valid_ctxt.is_null());
2595
2596 unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2597 unsafe { xmlSchematronFreeParserCtxt(schema) };
2598 }
2600
2601 #[test]
2602 fn test_c_abi_parse_free() {
2603 let schema_xml = r#"<?xml version="1.0"?>
2604<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2605 <pattern id="P1">
2606 <rule context="root">
2607 <assert test="true()">Test</assert>
2608 </rule>
2609 </pattern>
2610</schema>"#;
2611
2612 let ctxt = unsafe {
2613 xmlSchematronNewMemParserCtxt(
2614 schema_xml.as_ptr() as *const c_char,
2615 schema_xml.len() as c_int,
2616 )
2617 };
2618 assert!(!ctxt.is_null());
2619
2620 let schema = unsafe { xmlSchematronParse(ctxt) };
2621 assert!(!schema.is_null());
2622
2623 unsafe { xmlSchematronFree(schema) };
2624 }
2626
2627 #[test]
2628 fn test_c_abi_validate_doc() {
2629 let schema_xml = r#"<?xml version="1.0"?>
2630<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2631 <pattern id="P1">
2632 <rule context="root">
2633 <assert test="true()">Always passes</assert>
2634 </rule>
2635 </pattern>
2636</schema>"#;
2637
2638 let doc_xml = r#"<?xml version="1.0"?>
2639<root>Hello</root>"#;
2640
2641 let ctxt = unsafe {
2642 xmlSchematronNewMemParserCtxt(
2643 schema_xml.as_ptr() as *const c_char,
2644 schema_xml.len() as c_int,
2645 )
2646 };
2647 assert!(!ctxt.is_null());
2648
2649 let schema = unsafe { xmlSchematronParse(ctxt) };
2650 assert!(!schema.is_null());
2651
2652 let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2653 assert!(!valid_ctxt.is_null());
2654
2655 let doc = unsafe {
2656 crate::abi::exports_xml2::xmlReadMemory(
2657 doc_xml.as_ptr() as *const c_char,
2658 doc_xml.len() as c_int,
2659 b"test.xml\0".as_ptr() as *const c_char,
2660 ptr::null(),
2661 0,
2662 )
2663 };
2664 assert!(!doc.is_null());
2665
2666 let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2667 assert_eq!(result, 0, "Validation should pass (return 0)");
2668
2669 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2670 unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2671 unsafe { xmlSchematronFree(schema) };
2672 }
2673
2674 #[test]
2675 fn test_c_abi_validate_fail() {
2676 let schema_xml = r#"<?xml version="1.0"?>
2677<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2678 <pattern id="P1">
2679 <rule context="root">
2680 <assert test="false()">Always fails</assert>
2681 </rule>
2682 </pattern>
2683</schema>"#;
2684
2685 let doc_xml = r#"<?xml version="1.0"?>
2686<root>Hello</root>"#;
2687
2688 let ctxt = unsafe {
2689 xmlSchematronNewMemParserCtxt(
2690 schema_xml.as_ptr() as *const c_char,
2691 schema_xml.len() as c_int,
2692 )
2693 };
2694 assert!(!ctxt.is_null());
2695
2696 let schema = unsafe { xmlSchematronParse(ctxt) };
2697 assert!(!schema.is_null());
2698
2699 let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2700 assert!(!valid_ctxt.is_null());
2701
2702 let doc = unsafe {
2703 crate::abi::exports_xml2::xmlReadMemory(
2704 doc_xml.as_ptr() as *const c_char,
2705 doc_xml.len() as c_int,
2706 b"test.xml\0".as_ptr() as *const c_char,
2707 ptr::null(),
2708 0,
2709 )
2710 };
2711 assert!(!doc.is_null());
2712
2713 let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2714 assert!(result > 0, "Validation should fail (return > 0)");
2715
2716 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2717 unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2718 unsafe { xmlSchematronFree(schema) };
2719 }
2720
2721 #[test]
2722 fn test_c_abi_null_handling() {
2723 unsafe { xmlSchematronFree(ptr::null_mut()) };
2725 unsafe { xmlSchematronFreeParserCtxt(ptr::null_mut()) };
2726 unsafe { xmlSchematronFreeValidCtxt(ptr::null_mut()) };
2727
2728 let result = unsafe { xmlSchematronParse(ptr::null_mut()) };
2730 assert!(result.is_null());
2731
2732 let result = unsafe { xmlSchematronValidateDoc(ptr::null_mut(), ptr::null_mut()) };
2734 assert_eq!(result, -1);
2735 }
2736
2737 #[test]
2740 fn test_validate_null_doc() {
2741 let schema = SchematronSchema::new();
2742 let mut ctxt = SchematronValidCtxt::new();
2743 let valid = unsafe { schematron_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
2744 assert!(!valid);
2745 assert!(ctxt.nb_errors > 0);
2746 }
2747
2748 #[test]
2749 fn test_active_rules_default_phase() {
2750 let mut schema = SchematronSchema::new();
2751
2752 let rule = SchematronRule::new("root".to_string());
2753 schema.rules.insert("r1".to_string(), rule);
2754
2755 schema
2756 .pattern_groups
2757 .insert("p1".to_string(), vec!["r1".to_string()]);
2758 schema.pattern_order.push("p1".to_string());
2759
2760 let rules = schema.active_rules(None);
2761 assert_eq!(rules.len(), 1);
2762 }
2763
2764 #[test]
2765 fn test_active_rules_unknown_phase() {
2766 let mut schema = SchematronSchema::new();
2767
2768 let rule = SchematronRule::new("root".to_string());
2769 schema.rules.insert("r1".to_string(), rule);
2770
2771 schema
2772 .pattern_groups
2773 .insert("p1".to_string(), vec!["r1".to_string()]);
2774 schema.pattern_order.push("p1".to_string());
2775
2776 let rules = schema.active_rules(Some("nonexistent"));
2777 assert_eq!(rules.len(), 1, "Unknown phase should use all patterns");
2778 }
2779
2780 #[test]
2781 fn test_parse_schema_with_span_and_emph() {
2782 let schema_xml = r#"<?xml version="1.0"?>
2783<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2784 <pattern id="P1">
2785 <rule context="root">
2786 <assert test="true()">Message with <span class="x">inline</span> and <emph>emphasis</emph></assert>
2787 </rule>
2788 </pattern>
2789</schema>"#;
2790
2791 let result = schematron_parse(schema_xml);
2792 assert!(
2793 result.is_ok(),
2794 "Failed to parse schema with span/emph: {:?}",
2795 result.err()
2796 );
2797 let schema = result.unwrap();
2798 let rule = schema.rules.values().next().unwrap();
2799 let pat = &rule.patterns[0];
2800 assert!(
2802 pat.text.contains("inline"),
2803 "Text should include span content"
2804 );
2805 assert!(
2806 pat.text.contains("emphasis"),
2807 "Text should include emph content"
2808 );
2809 }
2810
2811 #[test]
2812 fn test_schematron_pattern_new_assert() {
2813 let pat = SchematronPattern::new(
2814 SchematronPatternType::Assert,
2815 "true()".to_string(),
2816 "Test message".to_string(),
2817 );
2818 assert_eq!(pat.pattern_type, SchematronPatternType::Assert);
2819 assert_eq!(pat.test, "true()");
2820 assert_eq!(pat.text, "Test message");
2821 assert!(pat.compiled_test.is_some());
2822 }
2823
2824 #[test]
2825 fn test_schematron_pattern_new_report() {
2826 let pat = SchematronPattern::new(
2827 SchematronPatternType::Report,
2828 "false()".to_string(),
2829 "Report message".to_string(),
2830 );
2831 assert_eq!(pat.pattern_type, SchematronPatternType::Report);
2832 assert!(pat.compiled_test.is_some());
2833 }
2834
2835 #[test]
2836 fn test_schematron_rule_new() {
2837 let rule = SchematronRule::new("root".to_string());
2838 assert_eq!(rule.context, "root");
2839 assert!(rule.patterns.is_empty());
2840 assert!(!rule.abstract_);
2841 }
2842
2843 #[test]
2844 fn test_schematron_schema_new() {
2845 let schema = SchematronSchema::new();
2846 assert_eq!(schema.query_binding, "xslt");
2847 assert!(schema.rules.is_empty());
2848 assert!(schema.phases.is_empty());
2849 assert!(schema.ns.is_empty());
2850 }
2851
2852 #[test]
2853 fn test_schematron_valid_ctxt_new() {
2854 let ctxt = SchematronValidCtxt::new();
2855 assert!(ctxt.errors.is_empty());
2856 assert_eq!(ctxt.nb_errors, 0);
2857 assert!(ctxt.active_phase.is_none());
2858 }
2859
2860 #[test]
2861 fn test_validate_assert_with_child_count() {
2862 let schema_xml = r#"<?xml version="1.0"?>
2863<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2864 <pattern id="P1">
2865 <rule context="root">
2866 <assert test="count(*) > 0">Root must have at least one child element</assert>
2867 </rule>
2868 </pattern>
2869</schema>"#;
2870
2871 let doc_xml = r#"<?xml version="1.0"?>
2872<root>
2873 <child>Content</child>
2874</root>"#;
2875
2876 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2877
2878 let doc = unsafe {
2879 crate::abi::exports_xml2::xmlReadMemory(
2880 doc_xml.as_ptr() as *const c_char,
2881 doc_xml.len() as c_int,
2882 b"test.xml\0".as_ptr() as *const c_char,
2883 ptr::null(),
2884 0,
2885 )
2886 };
2887 assert!(!doc.is_null());
2888
2889 let mut ctxt = SchematronValidCtxt::new();
2890 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2891 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2892
2893 assert!(valid, "Child count check failed: {:?}", ctxt.errors);
2894 }
2895}