1#![allow(missing_docs, non_snake_case, non_camel_case_types, non_upper_case_globals)]
29
30use core::ffi::c_void;
31use core::ptr;
32use std::collections::HashMap;
33use std::os::raw::{c_char, c_int};
34
35use crate::abi::allocator;
36use crate::abi::structs::*;
37use crate::abi::types::xmlElementType::*;
38use crate::abi::types::*;
39use crate::xml::xpath::ast::CompiledExpr;
40use crate::xml::xpath::context::XPathContext;
41use crate::xml::xpath::types::XPathValue;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum SchematronPatternType {
50 Assert,
52 Report,
54}
55
56#[derive(Debug, Clone)]
62pub struct SchematronPattern {
63 pub pattern_type: SchematronPatternType,
65 pub test: String,
67 pub compiled_test: Option<CompiledExpr>,
69 pub text: String,
71 pub flag: Option<String>,
73 pub role: Option<String>,
75 pub id: Option<String>,
77 pub icon: Option<String>,
79 pub see: Option<String>,
81 pub diagnostics: Option<String>,
83}
84
85impl SchematronPattern {
86 pub fn new(
88 pattern_type: SchematronPatternType,
89 test: String,
90 text: String,
91 ) -> Self {
92 Self {
93 pattern_type,
94 compiled_test: crate::xml::xpath::compile(&test),
95 test,
96 text,
97 flag: None,
98 role: None,
99 id: None,
100 icon: None,
101 see: None,
102 diagnostics: None,
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
117pub struct SchematronRule {
118 pub context: String,
120 pub compiled_context: Option<CompiledExpr>,
122 pub patterns: Vec<SchematronPattern>,
124 pub id: Option<String>,
126 pub abstract_: bool,
128 pub extends: Vec<String>,
130}
131
132impl SchematronRule {
133 pub fn new(context: String) -> Self {
135 Self {
136 compiled_context: crate::xml::xpath::compile(&context),
137 context,
138 patterns: Vec::new(),
139 id: None,
140 abstract_: false,
141 extends: Vec::new(),
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
156pub struct SchematronPhase {
157 pub id: String,
159 pub active_patterns: Vec<String>,
161}
162
163#[derive(Debug, Clone)]
169pub struct SchematronDiagnostic {
170 pub id: String,
172 pub text: String,
174 pub icon: Option<String>,
176 pub see: Option<String>,
178}
179
180#[derive(Debug, Clone)]
190pub struct SchematronSchema {
191 pub title: Option<String>,
193 pub phases: HashMap<String, SchematronPhase>,
195 pub rules: HashMap<String, SchematronRule>,
197 pub pattern_groups: HashMap<String, Vec<String>>,
199 pub pattern_order: Vec<String>,
201 pub ns: HashMap<String, String>,
203 pub query_binding: String,
205 pub default_phase: Option<String>,
207 pub diagnostics: HashMap<String, SchematronDiagnostic>,
209 pub errors: Vec<String>,
211}
212
213impl SchematronSchema {
214 pub fn new() -> Self {
216 Self {
217 title: None,
218 phases: HashMap::new(),
219 rules: HashMap::new(),
220 pattern_groups: HashMap::new(),
221 pattern_order: Vec::new(),
222 ns: HashMap::new(),
223 query_binding: "xslt".to_string(),
224 default_phase: None,
225 diagnostics: HashMap::new(),
226 errors: Vec::new(),
227 }
228 }
229
230 pub fn resolve_rule(&self, rule_id: &str) -> Option<SchematronRule> {
232 let rule = self.rules.get(rule_id)?.clone();
233 Some(self.resolve_extends(rule))
234 }
235
236 fn resolve_extends(&self, mut rule: SchematronRule) -> SchematronRule {
238 let extended_ids: Vec<String> = rule.extends.clone();
239 for ext_id in &extended_ids {
240 if let Some(ext_rule) = self.rules.get(ext_id) {
241 let resolved_ext = self.resolve_extends(ext_rule.clone());
243 rule.patterns.extend(resolved_ext.patterns);
244 }
245 }
246 rule
247 }
248
249 pub fn active_rules(&self, phase_id: Option<&str>) -> Vec<SchematronRule> {
253 let active_patterns: Vec<String> = match phase_id {
255 Some(pid) => {
256 if let Some(phase) = self.phases.get(pid) {
257 phase.active_patterns.clone()
258 } else {
259 self.pattern_order.clone()
261 }
262 }
263 None => {
264 if let Some(default) = &self.default_phase {
266 if let Some(phase) = self.phases.get(default) {
267 phase.active_patterns.clone()
268 } else {
269 self.pattern_order.clone()
270 }
271 } else {
272 self.pattern_order.clone()
273 }
274 }
275 };
276
277 let mut result = Vec::new();
278 for pat_id in &active_patterns {
279 if let Some(rule_ids) = self.pattern_groups.get(pat_id) {
280 for rule_id in rule_ids {
281 if let Some(rule) = self.rules.get(rule_id) {
282 if !rule.abstract_ {
283 result.push(self.resolve_extends(rule.clone()));
284 }
285 }
286 }
287 }
288 }
289
290 result
291 }
292}
293
294impl Default for SchematronSchema {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300#[derive(Debug)]
309pub struct SchematronValidCtxt {
310 pub schema: Option<SchematronSchema>,
312 pub errors: Vec<String>,
314 pub nb_errors: i32,
316 pub active_phase: Option<String>,
318}
319
320impl SchematronValidCtxt {
321 pub fn new() -> Self {
323 Self {
324 schema: None,
325 errors: Vec::new(),
326 nb_errors: 0,
327 active_phase: None,
328 }
329 }
330
331 pub fn record_error(&mut self, msg: String) {
333 self.errors.push(msg);
334 self.nb_errors += 1;
335 }
336}
337
338impl Default for SchematronValidCtxt {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344unsafe fn get_local_name(node: *mut _xmlNode) -> String {
354 if node.is_null() {
355 return String::new();
356 }
357 unsafe {
358 let name = (*node).name;
359 if name.is_null() {
360 return String::new();
361 }
362 let mut len = 0;
363 while *name.add(len) != 0 {
364 len += 1;
365 }
366 let slice = std::slice::from_raw_parts(name, len);
367 if let Ok(s) = std::str::from_utf8(slice) {
368 if let Some(pos) = s.find(':') {
369 s[pos + 1..].to_string()
370 } else {
371 s.to_string()
372 }
373 } else {
374 String::new()
375 }
376 }
377}
378
379unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
385 if node.is_null() {
386 return String::new();
387 }
388 unsafe {
389 let ns = (*node).ns;
390 let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
391 let mut len = 0;
392 while *(*ns).prefix.add(len) != 0 {
393 len += 1;
394 }
395 let slice = std::slice::from_raw_parts((*ns).prefix, len);
396 if let Ok(s) = std::str::from_utf8(slice) {
397 format!("{}:", s)
398 } else {
399 String::new()
400 }
401 } else {
402 String::new()
403 };
404
405 let name = (*node).name;
406 if name.is_null() {
407 return String::new();
408 }
409 let mut len = 0;
410 while *name.add(len) != 0 {
411 len += 1;
412 }
413 let slice = std::slice::from_raw_parts(name, len);
414 if let Ok(s) = std::str::from_utf8(slice) {
415 format!("{}{}", prefix, s)
416 } else {
417 String::new()
418 }
419 }
420}
421
422unsafe fn get_node_text(node: *mut _xmlNode) -> String {
428 if node.is_null() {
429 return String::new();
430 }
431 let mut result = String::new();
432 unsafe {
433 let mut child = (*node).children;
434 while !child.is_null() {
435 if (*child).type_ == XML_TEXT_NODE as c_int
436 || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
437 {
438 if !(*child).content.is_null() {
439 let content = (*child).content;
440 let mut len = 0;
441 while *content.add(len) != 0 {
442 len += 1;
443 }
444 let slice = std::slice::from_raw_parts(content, len);
445 result.push_str(&String::from_utf8_lossy(slice));
446 }
447 }
448 child = (*child).next;
449 }
450 }
451 result
452}
453
454unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
460 if node.is_null() {
461 return None;
462 }
463 unsafe {
464 let mut prop = (*node).properties;
465 while !prop.is_null() {
466 let prop_name = (*prop).name;
467 if !prop_name.is_null() {
468 let mut len = 0;
469 while *prop_name.add(len) != 0 {
470 len += 1;
471 }
472 let slice = std::slice::from_raw_parts(prop_name, len);
473 if let Ok(s) = std::str::from_utf8(slice) {
474 if s == name {
475 return Some(get_node_text(prop as *mut _xmlNode));
476 }
477 }
478 }
479 prop = (*prop).next;
480 }
481 }
482 None
483}
484
485unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
491 if node.is_null() {
492 return false;
493 }
494 unsafe {
495 let name = (*node).name;
496 if name.is_null() {
497 return false;
498 }
499 let mut len = 0;
500 while *name.add(len) != 0 {
501 len += 1;
502 }
503 let slice = std::slice::from_raw_parts(name, len);
504 if let Ok(s) = std::str::from_utf8(slice) {
505 let local = if let Some(pos) = s.find(':') {
506 &s[pos + 1..]
507 } else {
508 s
509 };
510 return local == local_name;
511 }
512 }
513 false
514}
515
516unsafe fn child_elements(node: *mut _xmlNode) -> Vec<*mut _xmlNode> {
522 let mut children = Vec::new();
523 if node.is_null() {
524 return children;
525 }
526 unsafe {
527 let mut child = (*node).children;
528 while !child.is_null() {
529 if (*child).type_ == XML_ELEMENT_NODE as c_int {
530 children.push(child);
531 }
532 child = (*child).next;
533 }
534 }
535 children
536}
537
538unsafe fn get_inline_text(node: *mut _xmlNode) -> String {
545 if node.is_null() {
546 return String::new();
547 }
548 let mut result = String::new();
549 unsafe {
550 let mut child = (*node).children;
551 while !child.is_null() {
552 if (*child).type_ == XML_TEXT_NODE as c_int
553 || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
554 {
555 if !(*child).content.is_null() {
556 let content = (*child).content;
557 let mut len = 0;
558 while *content.add(len) != 0 {
559 len += 1;
560 }
561 let slice = std::slice::from_raw_parts(content, len);
562 result.push_str(&String::from_utf8_lossy(slice));
563 }
564 } else if (*child).type_ == XML_ELEMENT_NODE as c_int {
565 let local = get_local_name(child);
566 match local.as_str() {
567 "span" | "emph" | "dir" => {
568 result.push_str(&get_inline_text(child));
569 }
570 _ => {}
571 }
572 }
573 child = (*child).next;
574 }
575 }
576 result
577}
578
579pub fn schematron_parse(xml_doc: &str) -> Result<SchematronSchema, String> {
592 let doc_ptr = unsafe {
593 crate::abi::exports_xml2::xmlReadMemory(
594 xml_doc.as_ptr() as *const c_char,
595 xml_doc.len() as c_int,
596 b"schema.sch\0".as_ptr() as *const c_char,
597 ptr::null(),
598 0,
599 )
600 };
601
602 if doc_ptr.is_null() {
603 return Err("Failed to parse Schematron schema XML document".to_string());
604 }
605
606 let result = unsafe { schematron_parse_doc(doc_ptr) };
607 unsafe {
608 crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
609 }
610 result
611}
612
613unsafe fn schematron_parse_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
619 unsafe {
620 let root = (*doc).children;
621 if root.is_null() {
622 return Err("Schematron document has no root element".to_string());
623 }
624
625 let mut root_elem = root;
627 while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
628 root_elem = (*root_elem).next;
629 }
630
631 if root_elem.is_null() {
632 return Err("Schematron document has no root element".to_string());
633 }
634
635 let local_name = get_local_name(root_elem);
636 if local_name != "schema" {
637 return Err(format!(
638 "Expected '<schema>' root element, found '<{}>'",
639 local_name
640 ));
641 }
642
643 Ok(schematron_parse_schema_node(root_elem))
644 }
645}
646
647unsafe fn schematron_parse_schema_node(node: *mut _xmlNode) -> SchematronSchema {
653 unsafe {
654 let mut schema = SchematronSchema::new();
655
656 if let Some(qb) = get_attr(node, "queryBinding") {
658 schema.query_binding = qb;
659 }
660 schema.title = get_attr(node, "title");
661 if let Some(df) = get_attr(node, "defaultPhase") {
662 schema.default_phase = Some(df);
663 }
664
665 let mut current_pattern_id: Option<String> = None;
667 let mut pattern_names: HashMap<String, Vec<String>> = HashMap::new();
668
669 let mut child = (*node).children;
670 while !child.is_null() {
671 if (*child).type_ == XML_ELEMENT_NODE as c_int {
672 let local = get_local_name(child);
673 match local.as_str() {
674 "title" => {
675 if schema.title.is_none() {
676 schema.title = Some(get_node_text(child).trim().to_string());
677 }
678 }
679 "ns" => {
680 let prefix = get_attr(child, "prefix").unwrap_or_default();
681 let uri = get_attr(child, "uri").unwrap_or_default();
682 if !prefix.is_empty() && !uri.is_empty() {
683 schema.ns.insert(prefix, uri);
684 }
685 }
686 "phase" => {
687 let phase = schematron_parse_phase(child);
688 schema.phases.insert(phase.id.clone(), phase);
689 }
690 "pattern" => {
691 let pat_id = schematron_parse_pattern_node(
692 child,
693 &mut schema,
694 &mut current_pattern_id,
695 &mut pattern_names,
696 );
697 current_pattern_id = pat_id;
698 }
699 "rule" => {
700 let rule = schematron_parse_rule(child, &mut schema);
702 let rule_id = rule.id.clone().unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
703 let rid = rule_id.clone();
705 schema.rules.insert(rid, rule);
706
707 if let Some(ref pid) = current_pattern_id {
709 schema
710 .pattern_groups
711 .entry(pid.clone())
712 .or_insert_with(Vec::new)
713 .push(rule_id);
714 } else {
715 let anon_id = format!("_anon_{}", schema.pattern_order.len());
717 schema
718 .pattern_groups
719 .entry(anon_id.clone())
720 .or_insert_with(Vec::new)
721 .push(rule_id);
722 if !schema.pattern_order.contains(&anon_id) {
723 schema.pattern_order.push(anon_id);
724 }
725 }
726 }
727 "diagnostics" => {
728 schematron_parse_diagnostics(child, &mut schema);
729 }
730 "include" => {
731 schematron_parse_include(child, &mut schema);
732 }
733 "p" | "caption" => {
734 }
736 _ => {
737 schema
738 .errors
739 .push(format!("Unexpected element '<{}>' in schema", local));
740 }
741 }
742 }
743 child = (*child).next;
744 }
745
746 schema
747 }
748}
749
750unsafe fn schematron_parse_pattern_node(
756 node: *mut _xmlNode,
757 schema: &mut SchematronSchema,
758 _current_pattern_id: &mut Option<String>,
759 _pattern_names: &mut HashMap<String, Vec<String>>,
760) -> Option<String> {
761 unsafe {
762 let pat_id = get_attr(node, "id");
763 let pat_name = get_attr(node, "name");
764 let pat_is_a = get_attr(node, "is-a");
765 let pat_see = get_attr(node, "see");
766 let pat_icon = get_attr(node, "icon");
767 let pat_role = get_attr(node, "role");
768
769 let pid = pat_id.clone().unwrap_or_else(|| {
770 format!("_pattern_{}", schema.pattern_order.len())
771 });
772
773 let mut rule_ids: Vec<String> = Vec::new();
774
775 let mut child = (*node).children;
777 while !child.is_null() {
778 if (*child).type_ == XML_ELEMENT_NODE as c_int {
779 let local = get_local_name(child);
780 match local.as_str() {
781 "rule" => {
782 let rule = schematron_parse_rule(child, schema);
783 let rule_id = rule.id.clone().unwrap_or_else(|| {
784 format!("_rule_{}", schema.rules.len())
785 });
786 let rid = rule_id.clone();
787 schema.rules.insert(rid, rule);
788 rule_ids.push(rule_id);
789 }
790 "p" | "caption" => {
791 }
793 _ => {
794 schema
795 .errors
796 .push(format!("Unexpected element '<{}>' in pattern", local));
797 }
798 }
799 }
800 child = (*child).next;
801 }
802
803 schema.pattern_groups.insert(pid.clone(), rule_ids);
804 schema.pattern_order.push(pid.clone());
805
806 if pat_is_a.is_some() {
808 }
811
812 let _ = pat_name;
814 let _ = pat_see;
815 let _ = pat_icon;
816 let _ = pat_role;
817
818 Some(pid)
819 }
820}
821
822unsafe fn schematron_parse_rule(
828 node: *mut _xmlNode,
829 schema: &mut SchematronSchema,
830) -> SchematronRule {
831 unsafe {
832 let context = get_attr(node, "context").unwrap_or_default();
833 let mut rule = SchematronRule::new(context);
834 rule.id = get_attr(node, "id");
835
836 let abs = get_attr(node, "abstract").unwrap_or_default();
837 rule.abstract_ = abs == "true" || abs == "1";
838
839 let mut child = (*node).children;
841 while !child.is_null() {
842 if (*child).type_ == XML_ELEMENT_NODE as c_int {
843 let local = get_local_name(child);
844 match local.as_str() {
845 "assert" => {
846 let pattern = schematron_parse_assert(child, SchematronPatternType::Assert);
847 rule.patterns.push(pattern);
848 }
849 "report" => {
850 let pattern = schematron_parse_assert(child, SchematronPatternType::Report);
851 rule.patterns.push(pattern);
852 }
853 "extends" => {
854 if let Some(ext_rule) = get_attr(child, "rule") {
855 rule.extends.push(ext_rule);
856 }
857 }
858 "let" => {
859 let name = get_attr(child, "name").unwrap_or_default();
862 let value = get_attr(child, "value").unwrap_or_default();
863 if !name.is_empty() {
864 let _ = value;
867 }
868 }
869 "param" => {
870 let _name = get_attr(child, "name");
872 let _value = get_attr(child, "value");
873 }
874 "p" | "caption" => {
875 }
877 _ => {
878 schema
879 .errors
880 .push(format!("Unexpected element '<{}>' in rule", local));
881 }
882 }
883 }
884 child = (*child).next;
885 }
886
887 rule
888 }
889}
890
891unsafe fn schematron_parse_assert(
897 node: *mut _xmlNode,
898 pattern_type: SchematronPatternType,
899) -> SchematronPattern {
900 unsafe {
901 let test = get_attr(node, "test").unwrap_or_default();
902 let text = get_inline_text(node);
903
904 let mut pattern = SchematronPattern::new(pattern_type, test, text);
905 pattern.flag = get_attr(node, "flag");
906 pattern.id = get_attr(node, "id");
907 pattern.icon = get_attr(node, "icon");
908 pattern.see = get_attr(node, "see");
909 pattern.role = get_attr(node, "role");
910 pattern.diagnostics = get_attr(node, "diagnostics");
911
912 pattern
916 }
917}
918
919unsafe fn schematron_parse_phase(node: *mut _xmlNode) -> SchematronPhase {
925 unsafe {
926 let id = get_attr(node, "id").unwrap_or_default();
927 let mut phase = SchematronPhase {
928 id,
929 active_patterns: Vec::new(),
930 };
931
932 let mut child = (*node).children;
933 while !child.is_null() {
934 if (*child).type_ == XML_ELEMENT_NODE as c_int {
935 let local = get_local_name(child);
936 if local == "active" {
937 if let Some(pattern) = get_attr(child, "pattern") {
938 phase.active_patterns.push(pattern);
939 }
940 }
941 }
942 child = (*child).next;
943 }
944
945 phase
946 }
947}
948
949unsafe fn schematron_parse_diagnostics(
955 node: *mut _xmlNode,
956 schema: &mut SchematronSchema,
957) {
958 unsafe {
959 let mut child = (*node).children;
960 while !child.is_null() {
961 if (*child).type_ == XML_ELEMENT_NODE as c_int {
962 let local = get_local_name(child);
963 if local == "diagnostic" {
964 let diag = schematron_parse_diagnostic(child);
965 schema.diagnostics.insert(diag.id.clone(), diag);
966 }
967 }
968 child = (*child).next;
969 }
970 }
971}
972
973unsafe fn schematron_parse_diagnostic(node: *mut _xmlNode) -> SchematronDiagnostic {
979 unsafe {
980 let id = get_attr(node, "id").unwrap_or_default();
981 let text = get_inline_text(node);
982 let icon = get_attr(node, "icon");
983 let see = get_attr(node, "see");
984
985 SchematronDiagnostic { id, text, icon, see }
986 }
987}
988
989unsafe fn schematron_parse_include(
995 node: *mut _xmlNode,
996 _schema: &mut SchematronSchema,
997) {
998 unsafe {
999 let href = get_attr(node, "href");
1000 if let Some(url) = href {
1001 let url_c = std::ffi::CString::new(url.clone()).ok();
1002 if let Some(c) = url_c {
1003 let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1004 if !doc.is_null() {
1005 let mut root = (*doc).children;
1007 while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
1008 root = (*root).next;
1009 }
1010 if !root.is_null() {
1011 let local = get_local_name(root);
1012 if local == "schema" || local == "pattern" || local == "rule" {
1013 }
1017 }
1018 crate::abi::exports_xml2::xmlFreeDoc(doc);
1019 }
1020 }
1021 }
1022 }
1023}
1024
1025unsafe fn expand_diagnostic_message(
1038 text: &str,
1039 context_node: *mut _xmlNode,
1040 xpath_ctxt: &mut XPathContext,
1041) -> String {
1042 let _ = context_node;
1054 let _ = xpath_ctxt;
1055 text.to_string()
1056}
1057
1058fn evaluate_xpath_boolean(
1064 compiled: &CompiledExpr,
1065 xpath_ctxt: &mut XPathContext,
1066) -> Result<bool, String> {
1067 match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1068 Some(value) => Ok(value.as_boolean()),
1069 None => Err("XPath evaluation failed".to_string()),
1070 }
1071}
1072
1073pub unsafe fn schematron_validate_doc(
1083 schema: &SchematronSchema,
1084 doc: *mut _xmlDoc,
1085 ctxt: &mut SchematronValidCtxt,
1086) -> bool {
1087 unsafe {
1088 if doc.is_null() {
1089 ctxt.record_error("Document is null".to_string());
1090 return false;
1091 }
1092
1093 let root = (*doc).children;
1094 if root.is_null() {
1095 ctxt.record_error("Document has no children".to_string());
1096 return false;
1097 }
1098
1099 let mut root_elem = root;
1101 while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
1102 root_elem = (*root_elem).next;
1103 }
1104
1105 if root_elem.is_null() {
1106 ctxt.record_error("Document has no root element".to_string());
1107 return false;
1108 }
1109
1110 let phase_id = ctxt.active_phase.as_deref();
1112 let rules = schema.active_rules(phase_id);
1113
1114 if rules.is_empty() {
1115 return true;
1117 }
1118
1119 let mut xpath_ctxt = XPathContext::new(doc);
1121
1122 let core_funcs = crate::xml::xpath::functions::core_functions();
1124 for (name, func) in core_funcs {
1125 xpath_ctxt.register_function(&name, func);
1126 }
1127
1128 for (prefix, uri) in &schema.ns {
1130 xpath_ctxt.namespaces.insert(prefix.clone(), uri.clone());
1131 }
1132
1133 let mut valid = true;
1134
1135 for rule in &rules {
1137 let matching_nodes: Vec<*mut _xmlNode> =
1139 find_matching_nodes(rule, root_elem, doc, &mut xpath_ctxt);
1140
1141 for context_node in &matching_nodes {
1142 xpath_ctxt.set_context_node(*context_node);
1144
1145 for pattern in &rule.patterns {
1147 let compiled = match &pattern.compiled_test {
1148 Some(c) => c,
1149 None => continue,
1150 };
1151
1152 let test_result = match evaluate_xpath_boolean(compiled, &mut xpath_ctxt) {
1153 Ok(val) => val,
1154 Err(e) => {
1155 ctxt.record_error(format!(
1156 "XPath error in '{}' test '{}': {}",
1157 if pattern.pattern_type == SchematronPatternType::Assert {
1158 "assert"
1159 } else {
1160 "report"
1161 },
1162 pattern.test,
1163 e
1164 ));
1165 valid = false;
1166 continue;
1167 }
1168 };
1169
1170 let message = expand_diagnostic_message(
1171 &pattern.text,
1172 *context_node,
1173 &mut xpath_ctxt,
1174 );
1175
1176 match pattern.pattern_type {
1177 SchematronPatternType::Assert => {
1178 if !test_result {
1180 let node_name = get_node_qname(*context_node);
1181 let flag_str = pattern
1182 .flag
1183 .as_ref()
1184 .map(|f| format!(" [{}]", f))
1185 .unwrap_or_default();
1186 let role_str = pattern
1187 .role
1188 .as_ref()
1189 .map(|r| format!(" ({})", r))
1190 .unwrap_or_default();
1191 let msg = if message.is_empty() {
1192 format!(
1193 "assertion failed: '{}' for node '{}'{}{}",
1194 pattern.test, node_name, flag_str, role_str
1195 )
1196 } else {
1197 format!(
1198 "assertion '{}' failed for node '{}'{}{}: {}",
1199 pattern.test, node_name, flag_str, role_str, message
1200 )
1201 };
1202 ctxt.record_error(msg);
1203 valid = false;
1204 }
1205 }
1206 SchematronPatternType::Report => {
1207 if test_result {
1209 let node_name = get_node_qname(*context_node);
1210 let flag_str = pattern
1211 .flag
1212 .as_ref()
1213 .map(|f| format!(" [{}]", f))
1214 .unwrap_or_default();
1215 let role_str = pattern
1216 .role
1217 .as_ref()
1218 .map(|r| format!(" ({})", r))
1219 .unwrap_or_default();
1220 let msg = if message.is_empty() {
1221 format!(
1222 "report triggered: '{}' for node '{}'{}{}",
1223 pattern.test, node_name, flag_str, role_str
1224 )
1225 } else {
1226 format!(
1227 "report '{}' triggered for node '{}'{}{}: {}",
1228 pattern.test, node_name, flag_str, role_str, message
1229 )
1230 };
1231 ctxt.record_error(msg);
1232 valid = false;
1233 }
1234 }
1235 }
1236 }
1237 }
1238 }
1239
1240 valid
1241 }
1242}
1243
1244unsafe fn find_matching_nodes(
1251 rule: &SchematronRule,
1252 root: *mut _xmlNode,
1253 doc: *mut _xmlDoc,
1254 xpath_ctxt: &mut XPathContext,
1255) -> Vec<*mut _xmlNode> {
1256 unsafe {
1257 if rule.context.is_empty() {
1259 let mut nodes = Vec::new();
1260 collect_all_elements(root, &mut nodes);
1261 return nodes;
1262 }
1263
1264 if let Some(compiled) = &rule.compiled_context {
1266 let is_simple_name = !rule.context.contains('/')
1269 && !rule.context.contains("::")
1270 && !rule.context.contains('[')
1271 && !rule.context.contains('(');
1272
1273 if !is_simple_name {
1274 xpath_ctxt.set_context_node(root);
1275 xpath_ctxt.document = doc;
1276
1277 match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1278 Some(XPathValue::NodeSet(ns)) => {
1279 if !ns.is_empty() {
1280 return ns.iter().collect();
1281 }
1282 }
1283 _ => {}
1284 }
1285 }
1286
1287 simple_context_match(&rule.context, root)
1289 } else {
1290 simple_context_match(&rule.context, root)
1292 }
1293 }
1294}
1295
1296fn simple_context_match(context: &str, root: *mut _xmlNode) -> Vec<*mut _xmlNode> {
1299 unsafe {
1300 let context = context.trim();
1301
1302 if context == "*" || context == "//*" {
1309 let mut nodes = Vec::new();
1310 collect_all_elements(root, &mut nodes);
1311 return nodes;
1312 }
1313
1314 if context.starts_with("//") {
1315 let name = &context[2..];
1316 if name.is_empty() || name == "*" {
1317 let mut nodes = Vec::new();
1318 collect_all_elements(root, &mut nodes);
1319 return nodes;
1320 }
1321 let mut nodes = Vec::new();
1323 collect_elements_by_name(root, name, &mut nodes);
1324 return nodes;
1325 }
1326
1327 if !context.contains('/') && !context.contains("::") {
1328 let mut nodes = Vec::new();
1330 let root_qname = get_node_qname(root);
1332 let root_local = get_local_name(root);
1333 if root_qname == context || root_local == context || context == "*" {
1334 nodes.push(root);
1335 }
1336 let mut child = (*root).children;
1338 while !child.is_null() {
1339 if (*child).type_ == XML_ELEMENT_NODE as c_int {
1340 let qname = get_node_qname(child);
1341 let local = get_local_name(child);
1342 if qname == context || local == context || context == "*" {
1343 nodes.push(child);
1344 }
1345 }
1346 child = (*child).next;
1347 }
1348 return nodes;
1349 }
1350
1351 vec![root]
1353 }
1354}
1355
1356unsafe fn collect_all_elements(node: *mut _xmlNode, nodes: &mut Vec<*mut _xmlNode>) {
1362 unsafe {
1363 if node.is_null() {
1364 return;
1365 }
1366 if (*node).type_ == XML_ELEMENT_NODE as c_int {
1367 nodes.push(node);
1368 }
1369 let mut child = (*node).children;
1370 while !child.is_null() {
1371 collect_all_elements(child, nodes);
1372 child = (*child).next;
1373 }
1374 }
1375}
1376
1377unsafe fn collect_elements_by_name(
1383 node: *mut _xmlNode,
1384 name: &str,
1385 nodes: &mut Vec<*mut _xmlNode>,
1386) {
1387 unsafe {
1388 if node.is_null() {
1389 return;
1390 }
1391 if (*node).type_ == XML_ELEMENT_NODE as c_int {
1392 let qname = get_node_qname(node);
1393 let local = get_local_name(node);
1394 if qname == name || local == name {
1395 nodes.push(node);
1396 }
1397 }
1398 let mut child = (*node).children;
1399 while !child.is_null() {
1400 collect_elements_by_name(child, name, nodes);
1401 child = (*child).next;
1402 }
1403 }
1404}
1405
1406pub fn schematron_parse_schema(xml_doc: &str) -> Result<SchematronSchema, String> {
1414 schematron_parse(xml_doc)
1415}
1416
1417pub unsafe fn schematron_parse_schema_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
1423 schematron_parse_doc(doc)
1424}
1425
1426pub unsafe fn schematron_validate_doc_schema(
1434 schema: &SchematronSchema,
1435 doc: *mut _xmlDoc,
1436 ctxt: &mut SchematronValidCtxt,
1437) -> bool {
1438 schematron_validate_doc(schema, doc, ctxt)
1439}
1440
1441#[no_mangle]
1460pub unsafe extern "C" fn xmlSchematronNewParserCtxt(url: *const c_char) -> *mut c_void {
1461 if url.is_null() {
1462 let ctxt = allocator::xmlMallocZero(std::mem::size_of::<SchematronSchema>() as usize);
1463 return ctxt;
1464 }
1465
1466 let url_str = unsafe {
1467 let mut len = 0;
1468 while *url.add(len) != 0 {
1469 len += 1;
1470 }
1471 let slice = std::slice::from_raw_parts(url as *const u8, len);
1472 String::from_utf8_lossy(slice).to_string()
1473 };
1474
1475 if !url_str.is_empty() {
1477 let url_c = std::ffi::CString::new(url_str.clone()).ok();
1478 if let Some(c) = url_c {
1479 let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1480 if !doc.is_null() {
1481 let result = schematron_parse_doc(doc);
1482 crate::abi::exports_xml2::xmlFreeDoc(doc);
1483 if let Ok(schema) = result {
1484 let schema_box = Box::new(schema);
1485 return Box::into_raw(schema_box) as *mut c_void;
1486 }
1487 }
1488 }
1489 }
1490
1491 let ctxt = allocator::xmlMallocZero(std::mem::size_of::<SchematronSchema>() as usize);
1493 ctxt
1494}
1495
1496#[no_mangle]
1508pub unsafe extern "C" fn xmlSchematronNewMemParserCtxt(
1509 buffer: *const c_char,
1510 size: c_int,
1511) -> *mut c_void {
1512 if buffer.is_null() || size <= 0 {
1513 return ptr::null_mut();
1514 }
1515
1516 let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
1518 let xml_str = String::from_utf8_lossy(buf_slice).to_string();
1519
1520 match schematron_parse(&xml_str) {
1521 Ok(schema) => {
1522 let schema_box = Box::new(schema);
1523 Box::into_raw(schema_box) as *mut c_void
1524 }
1525 Err(_) => ptr::null_mut(),
1526 }
1527}
1528
1529#[no_mangle]
1541pub unsafe extern "C" fn xmlSchematronParse(ctxt: *mut c_void) -> *mut c_void {
1542 if ctxt.is_null() {
1543 return ptr::null_mut();
1544 }
1545
1546 ctxt
1549}
1550
1551#[no_mangle]
1563pub unsafe extern "C" fn xmlSchematronFree(schema: *mut c_void) {
1564 if schema.is_null() {
1565 return;
1566 }
1567 unsafe {
1569 let _ = Box::from_raw(schema as *mut SchematronSchema);
1570 }
1571}
1572
1573#[no_mangle]
1585pub unsafe extern "C" fn xmlSchematronFreeParserCtxt(ctxt: *mut c_void) {
1586 if ctxt.is_null() {
1587 return;
1588 }
1589 unsafe {
1591 let _ = Box::from_raw(ctxt as *mut SchematronSchema);
1592 }
1593}
1594
1595#[no_mangle]
1607pub unsafe extern "C" fn xmlSchematronNewValidCtxt(schema: *mut c_void) -> *mut c_void {
1608 let mut ctxt = SchematronValidCtxt::new();
1609
1610 if !schema.is_null() {
1611 unsafe {
1613 let schema_ref = &*(schema as *const SchematronSchema);
1614 ctxt.schema = Some(schema_ref.clone());
1615 }
1616 }
1617
1618 let boxed = Box::new(ctxt);
1619 Box::into_raw(boxed) as *mut c_void
1620}
1621
1622#[no_mangle]
1634pub unsafe extern "C" fn xmlSchematronFreeValidCtxt(ctxt: *mut c_void) {
1635 if ctxt.is_null() {
1636 return;
1637 }
1638 unsafe {
1640 let _ = Box::from_raw(ctxt as *mut SchematronValidCtxt);
1641 }
1642}
1643
1644#[no_mangle]
1659pub unsafe extern "C" fn xmlSchematronValidateDoc(
1660 ctxt: *mut c_void,
1661 doc: *mut _xmlDoc,
1662) -> c_int {
1663 if ctxt.is_null() || doc.is_null() {
1664 return -1;
1665 }
1666
1667 unsafe {
1668 let valid_ctxt = &mut *(ctxt as *mut SchematronValidCtxt);
1669 let schema = match &valid_ctxt.schema {
1670 Some(s) => s,
1671 None => return -1,
1672 };
1673
1674 let mut temp_ctxt = SchematronValidCtxt::new();
1675 temp_ctxt.active_phase = valid_ctxt.active_phase.clone();
1676
1677 let valid = schematron_validate_doc(schema, doc, &mut temp_ctxt);
1678
1679 if valid {
1680 0
1681 } else {
1682 valid_ctxt.errors = temp_ctxt.errors;
1683 valid_ctxt.nb_errors = temp_ctxt.nb_errors;
1684 temp_ctxt.nb_errors
1685 }
1686 }
1687}
1688
1689#[cfg(test)]
1694mod tests {
1695 use super::*;
1696
1697 #[test]
1700 fn test_parse_simple_schema() {
1701 let schema_xml = r#"<?xml version="1.0"?>
1702<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1703 <pattern id="P1">
1704 <rule context="root">
1705 <assert test="count(*) > 0">Root must have children</assert>
1706 </rule>
1707 </pattern>
1708</schema>"#;
1709
1710 let result = schematron_parse(schema_xml);
1711 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1712 let schema = result.unwrap();
1713 assert_eq!(schema.pattern_order.len(), 1);
1714 assert_eq!(schema.rules.len(), 1);
1715 }
1716
1717 #[test]
1718 fn test_parse_with_ns() {
1719 let schema_xml = r#"<?xml version="1.0"?>
1720<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1721 <ns prefix="doc" uri="http://example.com/doc"/>
1722 <pattern id="P1">
1723 <rule context="doc:entry">
1724 <assert test="doc:title">Entry must have a title</assert>
1725 </rule>
1726 </pattern>
1727</schema>"#;
1728
1729 let result = schematron_parse(schema_xml);
1730 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1731 let schema = result.unwrap();
1732 assert!(schema.ns.contains_key("doc"));
1733 assert_eq!(schema.ns.get("doc").unwrap(), "http://example.com/doc");
1734 }
1735
1736 #[test]
1737 fn test_parse_with_phases() {
1738 let schema_xml = r#"<?xml version="1.0"?>
1739<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
1740 <phase id="phaseA">
1741 <active pattern="P1"/>
1742 </phase>
1743 <phase id="phaseB">
1744 <active pattern="P2"/>
1745 </phase>
1746 <pattern id="P1">
1747 <rule context="root">
1748 <assert test="true()">Always passes</assert>
1749 </rule>
1750 </pattern>
1751 <pattern id="P2">
1752 <rule context="root">
1753 <assert test="false()">Always fails</assert>
1754 </rule>
1755 </pattern>
1756</schema>"#;
1757
1758 let result = schematron_parse(schema_xml);
1759 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1760 let schema = result.unwrap();
1761 assert_eq!(schema.phases.len(), 2);
1762 assert!(schema.phases.contains_key("phaseA"));
1763 assert!(schema.phases.contains_key("phaseB"));
1764 assert_eq!(schema.default_phase.as_deref(), Some("phaseA"));
1765 }
1766
1767 #[test]
1768 fn test_parse_report_pattern() {
1769 let schema_xml = r#"<?xml version="1.0"?>
1770<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1771 <pattern id="P1">
1772 <rule context="root">
1773 <report test="@deprecated">Element is deprecated</report>
1774 </rule>
1775 </pattern>
1776</schema>"#;
1777
1778 let result = schematron_parse(schema_xml);
1779 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1780 let schema = result.unwrap();
1781 let rule = schema.rules.values().next().unwrap();
1782 assert_eq!(rule.patterns.len(), 1);
1783 assert_eq!(
1784 rule.patterns[0].pattern_type,
1785 SchematronPatternType::Report
1786 );
1787 assert_eq!(rule.patterns[0].test, "@deprecated");
1788 }
1789
1790 #[test]
1791 fn test_parse_abstract_rule() {
1792 let schema_xml = r#"<?xml version="1.0"?>
1793<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1794 <pattern id="P1">
1795 <rule id="abstractRule" abstract="true" context="*">
1796 <assert test="true()">Abstract assertion</assert>
1797 </rule>
1798 <rule id="concreteRule" context="root">
1799 <extends rule="abstractRule"/>
1800 <assert test="count(*) > 0">Concrete assertion</assert>
1801 </rule>
1802 </pattern>
1803</schema>"#;
1804
1805 let result = schematron_parse(schema_xml);
1806 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1807 let schema = result.unwrap();
1808 assert!(schema.rules.contains_key("abstractRule"));
1809 assert!(schema.rules.contains_key("concreteRule"));
1810 let abstract_rule = &schema.rules["abstractRule"];
1811 assert!(abstract_rule.abstract_);
1812 let concrete_rule = &schema.rules["concreteRule"];
1813 assert!(!concrete_rule.abstract_);
1814 assert_eq!(concrete_rule.extends.len(), 1);
1815 assert_eq!(concrete_rule.extends[0], "abstractRule");
1816 }
1817
1818 #[test]
1819 fn test_parse_with_diagnostics() {
1820 let schema_xml = r#"<?xml version="1.0"?>
1821<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1822 <diagnostics>
1823 <diagnostic id="diag1">This is a diagnostic message</diagnostic>
1824 </diagnostics>
1825 <pattern id="P1">
1826 <rule context="root">
1827 <assert test="true()" diagnostics="diag1">Assertion with diagnostic</assert>
1828 </rule>
1829 </pattern>
1830</schema>"#;
1831
1832 let result = schematron_parse(schema_xml);
1833 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1834 let schema = result.unwrap();
1835 assert!(schema.diagnostics.contains_key("diag1"));
1836 assert_eq!(
1837 schema.diagnostics["diag1"].text,
1838 "This is a diagnostic message"
1839 );
1840 }
1841
1842 #[test]
1843 fn test_parse_with_attributes() {
1844 let schema_xml = r#"<?xml version="1.0"?>
1845<schema xmlns="http://purl.oclc.org/dsdl/schematron" title="Test Schema">
1846 <pattern id="P1">
1847 <rule context="root">
1848 <assert test="true()" flag="warn" role="error" id="a1" icon="info" see="http://example.com">
1849 Test message
1850 </assert>
1851 </rule>
1852 </pattern>
1853</schema>"#;
1854
1855 let result = schematron_parse(schema_xml);
1856 assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1857 let schema = result.unwrap();
1858 assert_eq!(schema.title.as_deref(), Some("Test Schema"));
1859 let rule = schema.rules.values().next().unwrap();
1860 let pat = &rule.patterns[0];
1861 assert_eq!(pat.flag.as_deref(), Some("warn"));
1862 assert_eq!(pat.role.as_deref(), Some("error"));
1863 assert_eq!(pat.id.as_deref(), Some("a1"));
1864 assert_eq!(pat.icon.as_deref(), Some("info"));
1865 assert_eq!(pat.see.as_deref(), Some("http://example.com"));
1866 }
1867
1868 #[test]
1869 fn test_parse_empty_schema() {
1870 let schema_xml = r#"<?xml version="1.0"?>
1871<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1872</schema>"#;
1873
1874 let result = schematron_parse(schema_xml);
1875 assert!(result.is_ok(), "Failed to parse empty schema");
1876 let schema = result.unwrap();
1877 assert!(schema.rules.is_empty());
1878 assert!(schema.phases.is_empty());
1879 }
1880
1881 #[test]
1882 fn test_parse_no_assertions() {
1883 let schema_xml = r#"<?xml version="1.0"?>
1884<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1885 <pattern id="P1">
1886 <rule context="root">
1887 </rule>
1888 </pattern>
1889</schema>"#;
1890
1891 let result = schematron_parse(schema_xml);
1892 assert!(result.is_ok(), "Failed to parse schema with no assertions");
1893 let schema = result.unwrap();
1894 let rule = schema.rules.values().next().unwrap();
1895 assert!(rule.patterns.is_empty());
1896 }
1897
1898 #[test]
1899 fn test_parse_invalid_root_element() {
1900 let schema_xml = r#"<?xml version="1.0"?>
1901<not-schema xmlns="http://purl.oclc.org/dsdl/schematron">
1902</not-schema>"#;
1903
1904 let result = schematron_parse(schema_xml);
1905 assert!(result.is_err(), "Should fail with wrong root element");
1906 assert!(
1907 result.err().unwrap().contains("Expected '<schema>'"),
1908 "Error should mention expected schema element"
1909 );
1910 }
1911
1912 #[test]
1913 fn test_parse_empty_document_fails() {
1914 let result = schematron_parse("");
1915 assert!(result.is_err());
1916 }
1917
1918 #[test]
1919 fn test_parse_invalid_xml_fails() {
1920 let result = schematron_parse("not valid xml <<<");
1921 assert!(result.is_err());
1922 }
1923
1924 #[test]
1925 fn test_parse_schema_with_let_and_param() {
1926 let schema_xml = r#"<?xml version="1.0"?>
1927<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1928 <pattern id="P1">
1929 <rule context="root">
1930 <let name="x" value="42"/>
1931 <param name="debug" value="true"/>
1932 <assert test="true()">Test with let and param</assert>
1933 </rule>
1934 </pattern>
1935</schema>"#;
1936
1937 let result = schematron_parse(schema_xml);
1938 assert!(result.is_ok(), "Failed to parse schema with let/param: {:?}", result.err());
1939 let schema = result.unwrap();
1940 assert_eq!(schema.rules.len(), 1);
1941 }
1942
1943 #[test]
1944 fn test_parse_schema_with_documentation() {
1945 let schema_xml = r#"<?xml version="1.0"?>
1946<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1947 <p>This is documentation</p>
1948 <caption>Table caption</caption>
1949 <pattern id="P1">
1950 <p>Pattern documentation</p>
1951 <rule context="root">
1952 <p>Rule documentation</p>
1953 <assert test="true()">Real assertion</assert>
1954 </rule>
1955 </pattern>
1956</schema>"#;
1957
1958 let result = schematron_parse(schema_xml);
1959 assert!(result.is_ok(), "Failed to parse schema with documentation");
1960 let schema = result.unwrap();
1961 assert_eq!(schema.rules.len(), 1);
1962 }
1963
1964 #[test]
1967 fn test_validate_assert_pass() {
1968 let schema_xml = r#"<?xml version="1.0"?>
1969<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1970 <pattern id="P1">
1971 <rule context="root">
1972 <assert test="true()">Always passes</assert>
1973 </rule>
1974 </pattern>
1975</schema>"#;
1976
1977 let doc_xml = r#"<?xml version="1.0"?>
1978<root>Hello</root>"#;
1979
1980 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
1981
1982 let doc = unsafe {
1983 crate::abi::exports_xml2::xmlReadMemory(
1984 doc_xml.as_ptr() as *const c_char,
1985 doc_xml.len() as c_int,
1986 b"test.xml\0".as_ptr() as *const c_char,
1987 ptr::null(),
1988 0,
1989 )
1990 };
1991 assert!(!doc.is_null(), "Failed to parse document");
1992
1993 let mut ctxt = SchematronValidCtxt::new();
1994 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
1995 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
1996
1997 assert!(valid, "Validation failed: {:?}", ctxt.errors);
1998 }
1999
2000 #[test]
2001 fn test_validate_assert_fail() {
2002 let schema_xml = r#"<?xml version="1.0"?>
2003<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2004 <pattern id="P1">
2005 <rule context="root">
2006 <assert test="false()">Always fails</assert>
2007 </rule>
2008 </pattern>
2009</schema>"#;
2010
2011 let doc_xml = r#"<?xml version="1.0"?>
2012<root>Hello</root>"#;
2013
2014 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2015
2016 let doc = unsafe {
2017 crate::abi::exports_xml2::xmlReadMemory(
2018 doc_xml.as_ptr() as *const c_char,
2019 doc_xml.len() as c_int,
2020 b"test.xml\0".as_ptr() as *const c_char,
2021 ptr::null(),
2022 0,
2023 )
2024 };
2025 assert!(!doc.is_null());
2026
2027 let mut ctxt = SchematronValidCtxt::new();
2028 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2029 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2030
2031 assert!(!valid, "Validation should have failed, errors: {:?}", ctxt.errors);
2032 assert!(ctxt.nb_errors > 0);
2033 }
2034
2035 #[test]
2036 fn test_validate_report_pass() {
2037 let schema_xml = r#"<?xml version="1.0"?>
2038<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2039 <pattern id="P1">
2040 <rule context="root">
2041 <report test="false()">Report should not trigger</report>
2042 </rule>
2043 </pattern>
2044</schema>"#;
2045
2046 let doc_xml = r#"<?xml version="1.0"?>
2047<root>Hello</root>"#;
2048
2049 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2050
2051 let doc = unsafe {
2052 crate::abi::exports_xml2::xmlReadMemory(
2053 doc_xml.as_ptr() as *const c_char,
2054 doc_xml.len() as c_int,
2055 b"test.xml\0".as_ptr() as *const c_char,
2056 ptr::null(),
2057 0,
2058 )
2059 };
2060 assert!(!doc.is_null());
2061
2062 let mut ctxt = SchematronValidCtxt::new();
2063 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2064 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2065
2066 assert!(valid, "Report should not trigger: {:?}", ctxt.errors);
2067 }
2068
2069 #[test]
2070 fn test_validate_report_fail() {
2071 let schema_xml = r#"<?xml version="1.0"?>
2072<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2073 <pattern id="P1">
2074 <rule context="root">
2075 <report test="true()">Report should trigger</report>
2076 </rule>
2077 </pattern>
2078</schema>"#;
2079
2080 let doc_xml = r#"<?xml version="1.0"?>
2081<root>Hello</root>"#;
2082
2083 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2084
2085 let doc = unsafe {
2086 crate::abi::exports_xml2::xmlReadMemory(
2087 doc_xml.as_ptr() as *const c_char,
2088 doc_xml.len() as c_int,
2089 b"test.xml\0".as_ptr() as *const c_char,
2090 ptr::null(),
2091 0,
2092 )
2093 };
2094 assert!(!doc.is_null());
2095
2096 let mut ctxt = SchematronValidCtxt::new();
2097 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2098 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2099
2100 assert!(!valid, "Report should have triggered");
2101 assert!(ctxt.nb_errors > 0);
2102 }
2103
2104 #[test]
2105 fn test_validate_context_matching() {
2106 let schema_xml = r#"<?xml version="1.0"?>
2107<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2108 <pattern id="P1">
2109 <rule context="child">
2110 <assert test="true()">Child matches</assert>
2111 </rule>
2112 </pattern>
2113</schema>"#;
2114
2115 let doc_xml = r#"<?xml version="1.0"?>
2116<root>
2117 <child>A</child>
2118 <child>B</child>
2119</root>"#;
2120
2121 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2122
2123 let doc = unsafe {
2124 crate::abi::exports_xml2::xmlReadMemory(
2125 doc_xml.as_ptr() as *const c_char,
2126 doc_xml.len() as c_int,
2127 b"test.xml\0".as_ptr() as *const c_char,
2128 ptr::null(),
2129 0,
2130 )
2131 };
2132 assert!(!doc.is_null());
2133
2134 let mut ctxt = SchematronValidCtxt::new();
2135 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2136 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2137
2138 assert!(valid, "Context matching failed: {:?}", ctxt.errors);
2139 }
2140
2141 #[test]
2142 fn test_validate_multiple_rules() {
2143 let schema_xml = r#"<?xml version="1.0"?>
2144<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2145 <pattern id="P1">
2146 <rule context="root">
2147 <assert test="true()">Root passes</assert>
2148 </rule>
2149 </pattern>
2150 <pattern id="P2">
2151 <rule context="child">
2152 <assert test="true()">Child passes</assert>
2153 </rule>
2154 </pattern>
2155</schema>"#;
2156
2157 let doc_xml = r#"<?xml version="1.0"?>
2158<root>
2159 <child>Content</child>
2160</root>"#;
2161
2162 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2163
2164 let doc = unsafe {
2165 crate::abi::exports_xml2::xmlReadMemory(
2166 doc_xml.as_ptr() as *const c_char,
2167 doc_xml.len() as c_int,
2168 b"test.xml\0".as_ptr() as *const c_char,
2169 ptr::null(),
2170 0,
2171 )
2172 };
2173 assert!(!doc.is_null());
2174
2175 let mut ctxt = SchematronValidCtxt::new();
2176 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2177 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2178
2179 assert!(valid, "Multiple rules failed: {:?}", ctxt.errors);
2180 }
2181
2182 #[test]
2183 fn test_validate_with_phase_filtering() {
2184 let schema_xml = r#"<?xml version="1.0"?>
2185<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
2186 <phase id="phaseA">
2187 <active pattern="P1"/>
2188 </phase>
2189 <phase id="phaseB">
2190 <active pattern="P2"/>
2191 </phase>
2192 <pattern id="P1">
2193 <rule context="root">
2194 <assert test="true()">Always passes</assert>
2195 </rule>
2196 </pattern>
2197 <pattern id="P2">
2198 <rule context="root">
2199 <assert test="false()">Always fails</assert>
2200 </rule>
2201 </pattern>
2202</schema>"#;
2203
2204 let doc_xml = r#"<?xml version="1.0"?>
2205<root>Hello</root>"#;
2206
2207 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2208
2209 let doc = unsafe {
2210 crate::abi::exports_xml2::xmlReadMemory(
2211 doc_xml.as_ptr() as *const c_char,
2212 doc_xml.len() as c_int,
2213 b"test.xml\0".as_ptr() as *const c_char,
2214 ptr::null(),
2215 0,
2216 )
2217 };
2218 assert!(!doc.is_null());
2219
2220 let mut ctxt = SchematronValidCtxt::new();
2222 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2223 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2224
2225 assert!(valid, "Phase filtering should make validation pass: {:?}", ctxt.errors);
2226 }
2227
2228 #[test]
2229 fn test_validate_no_rules() {
2230 let schema_xml = r#"<?xml version="1.0"?>
2231<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2232</schema>"#;
2233
2234 let doc_xml = r#"<?xml version="1.0"?>
2235<root>Hello</root>"#;
2236
2237 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2238
2239 let doc = unsafe {
2240 crate::abi::exports_xml2::xmlReadMemory(
2241 doc_xml.as_ptr() as *const c_char,
2242 doc_xml.len() as c_int,
2243 b"test.xml\0".as_ptr() as *const c_char,
2244 ptr::null(),
2245 0,
2246 )
2247 };
2248 assert!(!doc.is_null());
2249
2250 let mut ctxt = SchematronValidCtxt::new();
2251 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2252 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2253
2254 assert!(valid, "Empty schema should pass validation");
2255 }
2256
2257 #[test]
2258 fn test_validate_extends_resolution() {
2259 let schema_xml = r#"<?xml version="1.0"?>
2260<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2261 <pattern id="P1">
2262 <rule id="base" abstract="true" context="*">
2263 <assert test="true()">Base assertion</assert>
2264 </rule>
2265 <rule id="derived" context="root">
2266 <extends rule="base"/>
2267 <assert test="true()">Derived assertion</assert>
2268 </rule>
2269 </pattern>
2270</schema>"#;
2271
2272 let doc_xml = r#"<?xml version="1.0"?>
2273<root>Hello</root>"#;
2274
2275 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2276
2277 let resolved = schema.resolve_rule("derived");
2279 assert!(resolved.is_some());
2280 let resolved = resolved.unwrap();
2281 assert_eq!(resolved.patterns.len(), 2, "Should have inherited the base pattern");
2283
2284 let doc = unsafe {
2285 crate::abi::exports_xml2::xmlReadMemory(
2286 doc_xml.as_ptr() as *const c_char,
2287 doc_xml.len() as c_int,
2288 b"test.xml\0".as_ptr() as *const c_char,
2289 ptr::null(),
2290 0,
2291 )
2292 };
2293 assert!(!doc.is_null());
2294
2295 let mut ctxt = SchematronValidCtxt::new();
2296 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2297 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2298
2299 assert!(valid, "Extends resolution failed: {:?}", ctxt.errors);
2300 }
2301
2302 #[test]
2303 fn test_validate_assert_with_flag() {
2304 let schema_xml = r#"<?xml version="1.0"?>
2305<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2306 <pattern id="P1">
2307 <rule context="root">
2308 <assert test="false()" flag="warn">Warning message</assert>
2309 </rule>
2310 </pattern>
2311</schema>"#;
2312
2313 let doc_xml = r#"<?xml version="1.0"?>
2314<root>Hello</root>"#;
2315
2316 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2317
2318 let doc = unsafe {
2319 crate::abi::exports_xml2::xmlReadMemory(
2320 doc_xml.as_ptr() as *const c_char,
2321 doc_xml.len() as c_int,
2322 b"test.xml\0".as_ptr() as *const c_char,
2323 ptr::null(),
2324 0,
2325 )
2326 };
2327 assert!(!doc.is_null());
2328
2329 let mut ctxt = SchematronValidCtxt::new();
2330 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2331 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2332
2333 assert!(!valid);
2334 assert!(ctxt.nb_errors > 0);
2335 assert!(ctxt.errors[0].contains("[warn]"), "Error should include flag");
2337 }
2338
2339 #[test]
2342 fn test_c_abi_new_free_parser_ctxt() {
2343 let ctxt = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2344 assert!(!ctxt.is_null());
2345 unsafe { xmlSchematronFreeParserCtxt(ctxt) };
2346 }
2348
2349 #[test]
2350 fn test_c_abi_new_free_valid_ctxt() {
2351 let schema = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2352 assert!(!schema.is_null());
2353
2354 let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2355 assert!(!valid_ctxt.is_null());
2356
2357 unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2358 unsafe { xmlSchematronFreeParserCtxt(schema) };
2359 }
2361
2362 #[test]
2363 fn test_c_abi_parse_free() {
2364 let schema_xml = r#"<?xml version="1.0"?>
2365<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2366 <pattern id="P1">
2367 <rule context="root">
2368 <assert test="true()">Test</assert>
2369 </rule>
2370 </pattern>
2371</schema>"#;
2372
2373 let ctxt = unsafe {
2374 xmlSchematronNewMemParserCtxt(
2375 schema_xml.as_ptr() as *const c_char,
2376 schema_xml.len() as c_int,
2377 )
2378 };
2379 assert!(!ctxt.is_null());
2380
2381 let schema = unsafe { xmlSchematronParse(ctxt) };
2382 assert!(!schema.is_null());
2383
2384 unsafe { xmlSchematronFree(schema) };
2385 }
2387
2388 #[test]
2389 fn test_c_abi_validate_doc() {
2390 let schema_xml = r#"<?xml version="1.0"?>
2391<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2392 <pattern id="P1">
2393 <rule context="root">
2394 <assert test="true()">Always passes</assert>
2395 </rule>
2396 </pattern>
2397</schema>"#;
2398
2399 let doc_xml = r#"<?xml version="1.0"?>
2400<root>Hello</root>"#;
2401
2402 let ctxt = unsafe {
2403 xmlSchematronNewMemParserCtxt(
2404 schema_xml.as_ptr() as *const c_char,
2405 schema_xml.len() as c_int,
2406 )
2407 };
2408 assert!(!ctxt.is_null());
2409
2410 let schema = unsafe { xmlSchematronParse(ctxt) };
2411 assert!(!schema.is_null());
2412
2413 let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2414 assert!(!valid_ctxt.is_null());
2415
2416 let doc = unsafe {
2417 crate::abi::exports_xml2::xmlReadMemory(
2418 doc_xml.as_ptr() as *const c_char,
2419 doc_xml.len() as c_int,
2420 b"test.xml\0".as_ptr() as *const c_char,
2421 ptr::null(),
2422 0,
2423 )
2424 };
2425 assert!(!doc.is_null());
2426
2427 let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2428 assert_eq!(result, 0, "Validation should pass (return 0)");
2429
2430 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2431 unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2432 unsafe { xmlSchematronFree(schema) };
2433 }
2434
2435 #[test]
2436 fn test_c_abi_validate_fail() {
2437 let schema_xml = r#"<?xml version="1.0"?>
2438<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2439 <pattern id="P1">
2440 <rule context="root">
2441 <assert test="false()">Always fails</assert>
2442 </rule>
2443 </pattern>
2444</schema>"#;
2445
2446 let doc_xml = r#"<?xml version="1.0"?>
2447<root>Hello</root>"#;
2448
2449 let ctxt = unsafe {
2450 xmlSchematronNewMemParserCtxt(
2451 schema_xml.as_ptr() as *const c_char,
2452 schema_xml.len() as c_int,
2453 )
2454 };
2455 assert!(!ctxt.is_null());
2456
2457 let schema = unsafe { xmlSchematronParse(ctxt) };
2458 assert!(!schema.is_null());
2459
2460 let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2461 assert!(!valid_ctxt.is_null());
2462
2463 let doc = unsafe {
2464 crate::abi::exports_xml2::xmlReadMemory(
2465 doc_xml.as_ptr() as *const c_char,
2466 doc_xml.len() as c_int,
2467 b"test.xml\0".as_ptr() as *const c_char,
2468 ptr::null(),
2469 0,
2470 )
2471 };
2472 assert!(!doc.is_null());
2473
2474 let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2475 assert!(result > 0, "Validation should fail (return > 0)");
2476
2477 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2478 unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2479 unsafe { xmlSchematronFree(schema) };
2480 }
2481
2482 #[test]
2483 fn test_c_abi_null_handling() {
2484 unsafe { xmlSchematronFree(ptr::null_mut()) };
2486 unsafe { xmlSchematronFreeParserCtxt(ptr::null_mut()) };
2487 unsafe { xmlSchematronFreeValidCtxt(ptr::null_mut()) };
2488
2489 let result = unsafe { xmlSchematronParse(ptr::null_mut()) };
2491 assert!(result.is_null());
2492
2493 let result = unsafe { xmlSchematronValidateDoc(ptr::null_mut(), ptr::null_mut()) };
2495 assert_eq!(result, -1);
2496 }
2497
2498 #[test]
2501 fn test_validate_null_doc() {
2502 let schema = SchematronSchema::new();
2503 let mut ctxt = SchematronValidCtxt::new();
2504 let valid = unsafe { schematron_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
2505 assert!(!valid);
2506 assert!(ctxt.nb_errors > 0);
2507 }
2508
2509 #[test]
2510 fn test_active_rules_default_phase() {
2511 let mut schema = SchematronSchema::new();
2512
2513 let rule = SchematronRule::new("root".to_string());
2514 schema.rules.insert("r1".to_string(), rule);
2515
2516 schema
2517 .pattern_groups
2518 .insert("p1".to_string(), vec!["r1".to_string()]);
2519 schema.pattern_order.push("p1".to_string());
2520
2521 let rules = schema.active_rules(None);
2522 assert_eq!(rules.len(), 1);
2523 }
2524
2525 #[test]
2526 fn test_active_rules_unknown_phase() {
2527 let mut schema = SchematronSchema::new();
2528
2529 let rule = SchematronRule::new("root".to_string());
2530 schema.rules.insert("r1".to_string(), rule);
2531
2532 schema
2533 .pattern_groups
2534 .insert("p1".to_string(), vec!["r1".to_string()]);
2535 schema.pattern_order.push("p1".to_string());
2536
2537 let rules = schema.active_rules(Some("nonexistent"));
2538 assert_eq!(rules.len(), 1, "Unknown phase should use all patterns");
2539 }
2540
2541 #[test]
2542 fn test_parse_schema_with_span_and_emph() {
2543 let schema_xml = r#"<?xml version="1.0"?>
2544<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2545 <pattern id="P1">
2546 <rule context="root">
2547 <assert test="true()">Message with <span class="x">inline</span> and <emph>emphasis</emph></assert>
2548 </rule>
2549 </pattern>
2550</schema>"#;
2551
2552 let result = schematron_parse(schema_xml);
2553 assert!(result.is_ok(), "Failed to parse schema with span/emph: {:?}", result.err());
2554 let schema = result.unwrap();
2555 let rule = schema.rules.values().next().unwrap();
2556 let pat = &rule.patterns[0];
2557 assert!(pat.text.contains("inline"), "Text should include span content");
2559 assert!(pat.text.contains("emphasis"), "Text should include emph content");
2560 }
2561
2562 #[test]
2563 fn test_schematron_pattern_new_assert() {
2564 let pat = SchematronPattern::new(
2565 SchematronPatternType::Assert,
2566 "true()".to_string(),
2567 "Test message".to_string(),
2568 );
2569 assert_eq!(pat.pattern_type, SchematronPatternType::Assert);
2570 assert_eq!(pat.test, "true()");
2571 assert_eq!(pat.text, "Test message");
2572 assert!(pat.compiled_test.is_some());
2573 }
2574
2575 #[test]
2576 fn test_schematron_pattern_new_report() {
2577 let pat = SchematronPattern::new(
2578 SchematronPatternType::Report,
2579 "false()".to_string(),
2580 "Report message".to_string(),
2581 );
2582 assert_eq!(pat.pattern_type, SchematronPatternType::Report);
2583 assert!(pat.compiled_test.is_some());
2584 }
2585
2586 #[test]
2587 fn test_schematron_rule_new() {
2588 let rule = SchematronRule::new("root".to_string());
2589 assert_eq!(rule.context, "root");
2590 assert!(rule.patterns.is_empty());
2591 assert!(!rule.abstract_);
2592 }
2593
2594 #[test]
2595 fn test_schematron_schema_new() {
2596 let schema = SchematronSchema::new();
2597 assert_eq!(schema.query_binding, "xslt");
2598 assert!(schema.rules.is_empty());
2599 assert!(schema.phases.is_empty());
2600 assert!(schema.ns.is_empty());
2601 }
2602
2603 #[test]
2604 fn test_schematron_valid_ctxt_new() {
2605 let ctxt = SchematronValidCtxt::new();
2606 assert!(ctxt.errors.is_empty());
2607 assert_eq!(ctxt.nb_errors, 0);
2608 assert!(ctxt.active_phase.is_none());
2609 }
2610
2611 #[test]
2612 fn test_validate_assert_with_child_count() {
2613 let schema_xml = r#"<?xml version="1.0"?>
2614<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2615 <pattern id="P1">
2616 <rule context="root">
2617 <assert test="count(*) > 0">Root must have at least one child element</assert>
2618 </rule>
2619 </pattern>
2620</schema>"#;
2621
2622 let doc_xml = r#"<?xml version="1.0"?>
2623<root>
2624 <child>Content</child>
2625</root>"#;
2626
2627 let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2628
2629 let doc = unsafe {
2630 crate::abi::exports_xml2::xmlReadMemory(
2631 doc_xml.as_ptr() as *const c_char,
2632 doc_xml.len() as c_int,
2633 b"test.xml\0".as_ptr() as *const c_char,
2634 ptr::null(),
2635 0,
2636 )
2637 };
2638 assert!(!doc.is_null());
2639
2640 let mut ctxt = SchematronValidCtxt::new();
2641 let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2642 unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2643
2644 assert!(valid, "Child count check failed: {:?}", ctxt.errors);
2645 }
2646}