#![allow(
missing_docs,
non_snake_case,
non_camel_case_types,
non_upper_case_globals
)]
use core::ffi::c_void;
use core::ptr;
use std::os::raw::{c_char, c_int};
use crate::abi::allocator;
use crate::abi::structs::*;
use crate::abi::types::xmlElementType::*;
#[derive(Debug, Clone, PartialEq)]
pub enum RelaxNgNameClass {
Name(String),
AnyName,
NsName(String),
Choice(Vec<RelaxNgNameClass>),
Except(Box<RelaxNgNameClass>, Box<RelaxNgNameClass>),
}
impl RelaxNgNameClass {
pub fn matches(&self, name: &str, ns_uri: Option<&str>) -> bool {
match self {
RelaxNgNameClass::Name(n) => name == n.as_str(),
RelaxNgNameClass::AnyName => true,
RelaxNgNameClass::NsName(ns) => {
if let Some(uri) = ns_uri {
uri == ns.as_str()
} else {
false
}
}
RelaxNgNameClass::Choice(choices) => choices.iter().any(|c| c.matches(name, ns_uri)),
RelaxNgNameClass::Except(positive, negative) => {
positive.matches(name, ns_uri) && !negative.matches(name, ns_uri)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelaxNgPatternType {
Element,
Attribute,
Text,
Choice,
Sequence,
Interleave,
ZeroOrMore,
OneOrMore,
Optional,
List,
Group,
Data,
Value,
Ref,
Define,
Grammar,
NotAllowed,
Empty,
ExternalRef,
Include,
Start,
}
#[derive(Debug, Clone)]
pub struct RelaxNgPattern {
pub pattern_type: RelaxNgPatternType,
pub name_class: Option<RelaxNgNameClass>,
pub children: Vec<RelaxNgPattern>,
pub name: Option<String>,
pub ns: Option<String>,
pub datatype: Option<String>,
pub value: Option<String>,
pub datatype_library: Option<String>,
}
impl RelaxNgPattern {
pub const fn new(pattern_type: RelaxNgPatternType) -> Self {
Self {
pattern_type,
name_class: None,
children: Vec::new(),
name: None,
ns: None,
datatype: None,
value: None,
datatype_library: None,
}
}
pub fn element(name: &str) -> Self {
let mut p = Self::new(RelaxNgPatternType::Element);
p.name_class = Some(RelaxNgNameClass::Name(name.to_string()));
p
}
pub fn attribute(name: &str) -> Self {
let mut p = Self::new(RelaxNgPatternType::Attribute);
p.name_class = Some(RelaxNgNameClass::Name(name.to_string()));
p
}
pub const fn text() -> Self {
Self::new(RelaxNgPatternType::Text)
}
pub const fn empty() -> Self {
Self::new(RelaxNgPatternType::Empty)
}
pub const fn not_allowed() -> Self {
Self::new(RelaxNgPatternType::NotAllowed)
}
}
#[derive(Debug, Clone)]
pub struct RelaxNgDefine {
pub name: String,
pub pattern: RelaxNgPattern,
}
#[derive(Debug, Clone)]
pub struct RelaxNgGrammar {
pub defines: Vec<RelaxNgDefine>,
pub start: Option<RelaxNgPattern>,
pub includes: Vec<RelaxNgGrammar>,
}
impl RelaxNgGrammar {
pub const fn new() -> Self {
Self {
defines: Vec::new(),
start: None,
includes: Vec::new(),
}
}
pub fn lookup(&self, name: &str) -> Option<&RelaxNgPattern> {
for def in &self.defines {
if def.name == name {
return Some(&def.pattern);
}
}
for inc in &self.includes {
if let Some(p) = inc.lookup(name) {
return Some(p);
}
}
None
}
}
impl Default for RelaxNgGrammar {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RelaxNgSchema {
pub grammar: RelaxNgGrammar,
pub errors: Vec<String>,
}
impl RelaxNgSchema {
pub const fn new() -> Self {
Self {
grammar: RelaxNgGrammar::new(),
errors: Vec::new(),
}
}
}
impl Default for RelaxNgSchema {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct RelaxNgValidCtxt {
pub schema: Option<RelaxNgSchema>,
pub errors: Vec<String>,
pub nb_errors: i32,
pub path: Vec<String>,
pub depth_max: i32,
pub depth: i32,
}
impl RelaxNgValidCtxt {
pub const fn new() -> Self {
Self {
schema: None,
errors: Vec::new(),
nb_errors: 0,
path: Vec::new(),
depth_max: 256,
depth: 0,
}
}
pub fn record_error(&mut self, msg: String) {
self.errors.push(msg);
self.nb_errors += 1;
}
pub fn current_path(&self) -> String {
if self.path.is_empty() {
"/".to_string()
} else {
format!("/{}", self.path.join("/"))
}
}
}
impl Default for RelaxNgValidCtxt {
fn default() -> Self {
Self::new()
}
}
unsafe fn get_local_name(node: *mut _xmlNode) -> String {
if node.is_null() {
return String::new();
}
unsafe {
let name = (*node).name;
if name.is_null() {
return String::new();
}
let mut len = 0;
while *name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(name, len);
if let Ok(s) = std::str::from_utf8(slice) {
if let Some(pos) = s.find(':') {
s[pos + 1..].to_string()
} else {
s.to_string()
}
} else {
String::new()
}
}
}
unsafe fn get_node_text(node: *mut _xmlNode) -> String {
if node.is_null() {
return String::new();
}
let mut result = String::new();
unsafe {
let mut child = (*node).children;
while !child.is_null() {
if ((*child).type_ == XML_TEXT_NODE as c_int
|| (*child).type_ == XML_CDATA_SECTION_NODE as c_int)
&& !(*child).content.is_null()
{
let content = (*child).content;
let mut len = 0;
while *content.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(content, len);
result.push_str(&String::from_utf8_lossy(slice));
}
child = (*child).next;
}
}
result
}
unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
if node.is_null() {
return String::new();
}
unsafe {
let ns = (*node).ns;
let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
let mut len = 0;
while *(*ns).prefix.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts((*ns).prefix, len);
if let Ok(s) = std::str::from_utf8(slice) {
format!("{}:", s)
} else {
String::new()
}
} else {
String::new()
};
let name = (*node).name;
if name.is_null() {
return String::new();
}
let mut len = 0;
while *name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(name, len);
if let Ok(s) = std::str::from_utf8(slice) {
format!("{}{}", prefix, s)
} else {
String::new()
}
}
}
unsafe fn get_node_ns_uri(node: *mut _xmlNode) -> Option<String> {
if node.is_null() {
return None;
}
unsafe {
let ns = (*node).ns;
if ns.is_null() || (*ns).href.is_null() {
return None;
}
let href = (*ns).href;
let mut len = 0;
while *href.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(href, len);
if let Ok(s) = std::str::from_utf8(slice) {
Some(s.to_string())
} else {
None
}
}
}
unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
if node.is_null() {
return None;
}
unsafe {
let mut prop = (*node).properties;
while !prop.is_null() {
let prop_name = (*prop).name;
if !prop_name.is_null() {
let mut len = 0;
while *prop_name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(prop_name, len);
if let Ok(s) = std::str::from_utf8(slice) {
if s == name {
return Some(get_node_text(prop as *mut _xmlNode));
}
}
}
prop = (*prop).next;
}
}
None
}
#[allow(dead_code)]
unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
if node.is_null() {
return false;
}
unsafe {
let name = (*node).name;
if name.is_null() {
return false;
}
let mut len = 0;
while *name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(name, len);
if let Ok(s) = std::str::from_utf8(slice) {
let local = if let Some(pos) = s.find(':') {
&s[pos + 1..]
} else {
s
};
return local == local_name;
}
}
false
}
pub fn rng_parse(xml_doc: &str) -> Result<RelaxNgSchema, String> {
let doc_ptr = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
xml_doc.as_ptr() as *const c_char,
xml_doc.len() as c_int,
c"schema.rng".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
if doc_ptr.is_null() {
return Err("Failed to parse RELAX NG schema XML document".to_string());
}
let result = unsafe { rng_parse_doc(doc_ptr) };
unsafe {
crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
}
result
}
unsafe fn rng_parse_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
unsafe {
let root = (*doc).children;
if root.is_null() {
return Err("RELAX NG document has no root element".to_string());
}
let mut root_elem = root;
while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
root_elem = (*root_elem).next;
}
if root_elem.is_null() {
return Err("RELAX NG document has no root element".to_string());
}
let local_name = get_local_name(root_elem);
let mut schema = RelaxNgSchema::new();
match local_name.as_str() {
"grammar" => {
schema.grammar = rng_parse_grammar_node(root_elem, &mut schema);
Ok(schema)
}
"element" | "attribute" | "text" | "choice" | "sequence" | "interleave"
| "zeroOrMore" | "oneOrMore" | "optional" | "list" | "group" | "data" | "value"
| "ref" | "notAllowed" | "empty" | "externalRef" | "define" | "start" | "include" => {
let pattern = rng_parse_pattern(root_elem, &mut schema);
schema.grammar.start = Some(pattern);
Ok(schema)
}
_ => Err(format!("Unknown RELAX NG root element: '{}'", local_name)),
}
}
}
unsafe fn rng_parse_grammar_node(
node: *mut _xmlNode,
schema: &mut RelaxNgSchema,
) -> RelaxNgGrammar {
unsafe {
let mut grammar = RelaxNgGrammar::new();
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let local = get_local_name(child);
match local.as_str() {
"define" => {
let def = rng_parse_define(child, schema);
grammar.defines.push(def);
}
"start" => {
grammar.start = Some(rng_parse_pattern(child, schema));
}
"include" => {
if let Some(inc) = rng_parse_include(child, schema) {
grammar.includes.push(inc);
}
}
"div" => {
let sub_grammar = rng_parse_grammar_node(child, schema);
grammar.defines.extend(sub_grammar.defines);
if sub_grammar.start.is_some() {
grammar.start = sub_grammar.start;
}
grammar.includes.extend(sub_grammar.includes);
}
_ => {
schema
.errors
.push(format!("Unexpected element '<{}>' in grammar", local));
}
}
}
child = (*child).next;
}
grammar
}
}
unsafe fn rng_parse_define(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgDefine {
unsafe {
let name = get_attr(node, "name").unwrap_or_default();
let pattern = rng_parse_pattern(node, schema);
RelaxNgDefine { name, pattern }
}
}
unsafe fn rng_parse_include(
node: *mut _xmlNode,
_schema: &mut RelaxNgSchema,
) -> Option<RelaxNgGrammar> {
unsafe {
let href = get_attr(node, "href");
if let Some(url) = href {
let url_c = std::ffi::CString::new(url.clone()).ok()?;
let doc = crate::abi::exports_xml2::xmlParseFile(url_c.as_ptr());
if doc.is_null() {
return None;
}
let mut inc_schema = RelaxNgSchema::new();
let grammar = rng_parse_grammar_node(
{
let mut root = (*doc).children;
while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
root = (*root).next;
}
root
},
&mut inc_schema,
);
crate::abi::exports_xml2::xmlFreeDoc(doc);
Some(grammar)
} else {
let mut inc_schema = RelaxNgSchema::new();
let grammar = rng_parse_grammar_node(node, &mut inc_schema);
Some(grammar)
}
}
}
unsafe fn rng_parse_pattern(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgPattern {
unsafe {
let local = get_local_name(node);
match local.as_str() {
"element" => rng_parse_element_pattern(node, schema),
"attribute" => rng_parse_attribute_pattern(node, schema),
"text" => RelaxNgPattern::text(),
"empty" => RelaxNgPattern::empty(),
"notAllowed" => RelaxNgPattern::not_allowed(),
"choice" => rng_parse_composite_pattern(node, RelaxNgPatternType::Choice, schema),
"sequence" => rng_parse_composite_pattern(node, RelaxNgPatternType::Sequence, schema),
"interleave" => {
rng_parse_composite_pattern(node, RelaxNgPatternType::Interleave, schema)
}
"zeroOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::ZeroOrMore, schema),
"oneOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::OneOrMore, schema),
"optional" => rng_parse_unary_pattern(node, RelaxNgPatternType::Optional, schema),
"list" => rng_parse_unary_pattern(node, RelaxNgPatternType::List, schema),
"group" => rng_parse_composite_pattern(node, RelaxNgPatternType::Group, schema),
"data" => rng_parse_data_pattern(node, schema),
"value" => rng_parse_value_pattern(node, schema),
"ref" => rng_parse_ref_pattern(node),
"externalRef" => rng_parse_external_ref(node, schema),
"define" | "start" | "grammar" | "include" | "div" => {
let mut child = (*node).children;
let mut result = RelaxNgPattern::empty();
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
result = rng_parse_pattern(child, schema);
break;
}
child = (*child).next;
}
result
}
_ => {
schema
.errors
.push(format!("Unknown pattern element '<{}>'", local));
RelaxNgPattern::empty()
}
}
}
}
unsafe fn rng_parse_element_pattern(
node: *mut _xmlNode,
schema: &mut RelaxNgSchema,
) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Element);
let name_attr = get_attr(node, "name");
pattern.name = name_attr.clone();
if let Some(ref n) = name_attr {
pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
}
let mut child = (*node).children;
let mut content_found = false;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let child_local = get_local_name(child);
match child_local.as_str() {
"name" => {
let text = get_node_text(child);
if !text.is_empty() {
pattern.name_class =
Some(RelaxNgNameClass::Name(text.trim().to_string()));
}
}
"anyName" => {
pattern.name_class = Some(rng_parse_any_name(child));
}
"nsName" => {
pattern.name_class = Some(rng_parse_ns_name(child));
}
"choice" if pattern.name_class.is_none() => {
pattern.name_class = Some(rng_parse_name_class_choice(child));
}
_ => {
if !content_found {
pattern.children.push(rng_parse_pattern(child, schema));
content_found = true;
}
}
}
}
child = (*child).next;
}
pattern
}
}
unsafe fn rng_parse_attribute_pattern(
node: *mut _xmlNode,
schema: &mut RelaxNgSchema,
) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Attribute);
let name_attr = get_attr(node, "name");
pattern.name = name_attr.clone();
if let Some(ref n) = name_attr {
pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
}
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let child_local = get_local_name(child);
match child_local.as_str() {
"name" => {
let text = get_node_text(child);
if !text.is_empty() {
pattern.name_class =
Some(RelaxNgNameClass::Name(text.trim().to_string()));
}
}
"anyName" => {
pattern.name_class = Some(rng_parse_any_name(child));
}
"nsName" => {
pattern.name_class = Some(rng_parse_ns_name(child));
}
"choice" if pattern.name_class.is_none() => {
pattern.name_class = Some(rng_parse_name_class_choice(child));
}
_ => {
pattern.children.push(rng_parse_pattern(child, schema));
}
}
}
child = (*child).next;
}
pattern
}
}
unsafe fn rng_parse_any_name(node: *mut _xmlNode) -> RelaxNgNameClass {
unsafe {
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
let except_nc = rng_parse_name_class_content(child);
return RelaxNgNameClass::Except(
Box::new(RelaxNgNameClass::AnyName),
Box::new(except_nc),
);
}
child = (*child).next;
}
RelaxNgNameClass::AnyName
}
}
unsafe fn rng_parse_ns_name(node: *mut _xmlNode) -> RelaxNgNameClass {
unsafe {
let ns = get_attr(node, "ns").unwrap_or_default();
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
let except_nc = rng_parse_name_class_content(child);
return RelaxNgNameClass::Except(
Box::new(RelaxNgNameClass::NsName(ns)),
Box::new(except_nc),
);
}
child = (*child).next;
}
RelaxNgNameClass::NsName(ns)
}
}
unsafe fn rng_parse_name_class_choice(node: *mut _xmlNode) -> RelaxNgNameClass {
unsafe {
let mut choices = Vec::new();
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
choices.push(rng_parse_name_class_item(child));
}
child = (*child).next;
}
if choices.len() == 1 {
choices.remove(0)
} else {
RelaxNgNameClass::Choice(choices)
}
}
}
unsafe fn rng_parse_name_class_item(node: *mut _xmlNode) -> RelaxNgNameClass {
unsafe {
let local = get_local_name(node);
match local.as_str() {
"name" => {
let text = get_node_text(node);
RelaxNgNameClass::Name(text.trim().to_string())
}
"anyName" => rng_parse_any_name(node),
"nsName" => rng_parse_ns_name(node),
"choice" => rng_parse_name_class_choice(node),
_ => {
let text = get_node_text(node);
if text.trim().is_empty() {
RelaxNgNameClass::AnyName
} else {
RelaxNgNameClass::Name(text.trim().to_string())
}
}
}
}
}
unsafe fn rng_parse_name_class_content(node: *mut _xmlNode) -> RelaxNgNameClass {
unsafe {
let mut names = Vec::new();
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
names.push(rng_parse_name_class_item(child));
}
child = (*child).next;
}
if names.is_empty() {
RelaxNgNameClass::AnyName
} else if names.len() == 1 {
names.remove(0)
} else {
RelaxNgNameClass::Choice(names)
}
}
}
unsafe fn rng_parse_composite_pattern(
node: *mut _xmlNode,
pattern_type: RelaxNgPatternType,
schema: &mut RelaxNgSchema,
) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(pattern_type);
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
pattern.children.push(rng_parse_pattern(child, schema));
}
child = (*child).next;
}
pattern
}
}
unsafe fn rng_parse_unary_pattern(
node: *mut _xmlNode,
pattern_type: RelaxNgPatternType,
schema: &mut RelaxNgSchema,
) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(pattern_type);
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
pattern.children.push(rng_parse_pattern(child, schema));
break;
}
child = (*child).next;
}
pattern
}
}
unsafe fn rng_parse_data_pattern(
node: *mut _xmlNode,
_schema: &mut RelaxNgSchema,
) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Data);
pattern.datatype = get_attr(node, "type");
pattern.datatype_library = get_attr(node, "datatypeLibrary");
pattern
}
}
unsafe fn rng_parse_value_pattern(
node: *mut _xmlNode,
_schema: &mut RelaxNgSchema,
) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Value);
pattern.datatype = get_attr(node, "type");
pattern.datatype_library = get_attr(node, "datatypeLibrary");
pattern.value = Some(get_node_text(node).trim().to_string());
pattern
}
}
unsafe fn rng_parse_ref_pattern(node: *mut _xmlNode) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Ref);
pattern.name = get_attr(node, "name");
pattern
}
}
unsafe fn rng_parse_external_ref(
node: *mut _xmlNode,
_schema: &mut RelaxNgSchema,
) -> RelaxNgPattern {
unsafe {
let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::ExternalRef);
let href = get_attr(node, "href");
pattern.name = href;
pattern
}
}
pub unsafe fn rng_validate_doc(
schema: &RelaxNgSchema,
doc: *mut _xmlDoc,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if doc.is_null() {
ctxt.record_error("Document is null".to_string());
return false;
}
let root = (*doc).children;
if root.is_null() {
ctxt.record_error("Document has no children".to_string());
return false;
}
let mut root_elem = root;
while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
root_elem = (*root_elem).next;
}
if root_elem.is_null() {
ctxt.record_error("Document has no root element".to_string());
return false;
}
let start_pattern = match &schema.grammar.start {
Some(p) => p,
None => {
ctxt.record_error("Schema has no start pattern".to_string());
return false;
}
};
ctxt.path.clear();
let valid = rng_validate_pattern(start_pattern, root_elem, schema, ctxt);
valid
}
}
fn rng_validate_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if ctxt.depth >= ctxt.depth_max {
ctxt.record_error("Maximum validation depth exceeded".to_string());
return false;
}
ctxt.depth += 1;
let result = match pattern.pattern_type {
RelaxNgPatternType::Element => {
rng_validate_element_pattern(pattern, node, schema, ctxt)
}
RelaxNgPatternType::Attribute => {
rng_validate_attribute_pattern(pattern, node, schema, ctxt)
}
RelaxNgPatternType::Text => rng_validate_text_pattern(node, ctxt),
RelaxNgPatternType::Empty => rng_validate_empty_pattern(node, ctxt),
RelaxNgPatternType::NotAllowed => {
let name = get_node_qname(node);
ctxt.record_error(format!(
"Element '{}' is not allowed at '{}'",
name,
ctxt.current_path()
));
false
}
RelaxNgPatternType::Choice => rng_validate_choice_pattern(pattern, node, schema, ctxt),
RelaxNgPatternType::Sequence => {
rng_validate_sequence_pattern(pattern, node, schema, ctxt)
}
RelaxNgPatternType::Interleave => {
rng_validate_interleave_pattern(pattern, node, schema, ctxt)
}
RelaxNgPatternType::ZeroOrMore => {
rng_validate_zero_or_more(pattern, node, schema, ctxt)
}
RelaxNgPatternType::OneOrMore => rng_validate_one_or_more(pattern, node, schema, ctxt),
RelaxNgPatternType::Optional => {
rng_validate_optional_pattern(pattern, node, schema, ctxt)
}
RelaxNgPatternType::List => rng_validate_list_pattern(pattern, node, schema, ctxt),
RelaxNgPatternType::Group => rng_validate_group_pattern(pattern, node, schema, ctxt),
RelaxNgPatternType::Data => rng_validate_data_pattern(pattern, node, ctxt),
RelaxNgPatternType::Value => rng_validate_value_pattern(pattern, node, ctxt),
RelaxNgPatternType::Ref => rng_validate_ref_pattern(pattern, node, schema, ctxt),
RelaxNgPatternType::ExternalRef => {
rng_validate_empty_pattern(node, ctxt)
}
RelaxNgPatternType::Define
| RelaxNgPatternType::Grammar
| RelaxNgPatternType::Start
| RelaxNgPatternType::Include => {
rng_validate_children(pattern, node, schema, ctxt)
}
};
ctxt.depth -= 1;
result
}
}
fn rng_validate_children(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
{
if pattern.children.is_empty() {
return true;
}
let mut valid = true;
for child in &pattern.children {
valid &= rng_validate_pattern(child, node, schema, ctxt);
}
valid
}
}
fn rng_validate_element_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
return false;
}
let node_name = get_node_qname(node);
let ns_uri = get_node_ns_uri(node);
if let Some(ref nc) = pattern.name_class {
if !nc.matches(&node_name, ns_uri.as_deref()) {
let pat_name = pattern.name.as_deref().unwrap_or("?");
ctxt.record_error(format!(
"Element '{}' does not match expected element pattern '{}' at '{}'",
node_name,
pat_name,
ctxt.current_path()
));
return false;
}
}
ctxt.path.push(node_name.clone());
let mut valid = true;
if pattern.children.is_empty() {
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let child_name = get_node_qname(child);
ctxt.record_error(format!(
"Unexpected child element '{}' in empty element '{}' at '{}'",
child_name,
node_name,
ctxt.current_path()
));
valid = false;
}
child = (*child).next;
}
} else {
for child_pat in &pattern.children {
valid &= rng_validate_pattern(child_pat, node, schema, ctxt);
}
}
ctxt.path.pop();
valid
}
}
fn rng_validate_attribute_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
_schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
return false;
}
let attr_name = match &pattern.name_class {
Some(RelaxNgNameClass::Name(n)) => n.clone(),
Some(RelaxNgNameClass::AnyName) => {
return true;
}
Some(RelaxNgNameClass::NsName(ns)) => {
let mut prop = (*node).properties;
while !prop.is_null() {
let prop_ns = get_node_ns_uri(prop as *mut _xmlNode);
if let Some(ref uri) = prop_ns {
if uri == ns {
if let Some(content) = &pattern.children.first() {
let val = get_node_text(prop as *mut _xmlNode);
let valid = match content.pattern_type {
RelaxNgPatternType::Text => true,
RelaxNgPatternType::Data => rng_validate_datatype_value(
content.datatype.as_deref(),
&val,
),
RelaxNgPatternType::Value => {
content.value.as_deref() == Some(&val)
}
_ => true,
};
if !valid {
ctxt.record_error(format!(
"Attribute '{}' has invalid value at '{}'",
prop_ns.unwrap_or_default(),
ctxt.current_path()
));
return false;
}
}
return true;
}
}
prop = (*prop).next;
}
ctxt.record_error(format!(
"Required attribute in namespace '{}' is missing at '{}'",
ns,
ctxt.current_path()
));
return false;
}
_ => {
return true;
}
};
let attr_value = get_attr(node, &attr_name);
match attr_value {
Some(val) => {
if let Some(content) = pattern.children.first() {
let valid = match content.pattern_type {
RelaxNgPatternType::Text => true,
RelaxNgPatternType::Data => {
rng_validate_datatype_value(content.datatype.as_deref(), &val)
}
RelaxNgPatternType::Value => content.value.as_deref() == Some(&val),
_ => true,
};
if !valid {
ctxt.record_error(format!(
"Attribute '{}' has invalid value '{}' at '{}'",
attr_name,
val,
ctxt.current_path()
));
return false;
}
}
true
}
None => {
ctxt.record_error(format!(
"Required attribute '{}' is missing at '{}'",
attr_name,
ctxt.current_path()
));
false
}
}
}
}
const fn rng_validate_text_pattern(node: *mut _xmlNode, _ctxt: &mut RelaxNgValidCtxt) -> bool {
{
if node.is_null() {
return false;
}
true
}
}
fn rng_validate_empty_pattern(node: *mut _xmlNode, ctxt: &mut RelaxNgValidCtxt) -> bool {
unsafe {
if node.is_null() {
return true;
}
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let child_name = get_node_qname(child);
ctxt.record_error(format!(
"Unexpected child element '{}' in empty content at '{}'",
child_name,
ctxt.current_path()
));
return false;
}
child = (*child).next;
}
true
}
}
fn rng_validate_choice_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if pattern.children.is_empty() {
return false;
}
let mut last_error = String::new();
for child in &pattern.children {
let saved_errors = ctxt.errors.len();
let saved_nb = ctxt.nb_errors;
if rng_validate_pattern(child, node, schema, ctxt) {
return true;
}
if ctxt.errors.len() > saved_errors {
last_error = ctxt.errors.last().unwrap().clone();
}
ctxt.errors.truncate(saved_errors);
ctxt.nb_errors = saved_nb;
}
let node_name = if node.is_null() {
"null".to_string()
} else {
get_node_qname(node)
};
ctxt.record_error(format!(
"No choice pattern matched for '{}' at '{}'. Last error: {}",
node_name,
ctxt.current_path(),
last_error
));
false
}
}
fn rng_validate_sequence_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() {
return pattern.children.is_empty();
}
let mut valid = true;
let mut child_nodes: Vec<*mut _xmlNode> = Vec::new();
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
child_nodes.push(child);
}
child = (*child).next;
}
let mut child_idx = 0;
for child_pat in &pattern.children {
if child_idx >= child_nodes.len() {
match child_pat.pattern_type {
RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
continue;
}
RelaxNgPatternType::OneOrMore => {
ctxt.record_error(format!(
"Expected at least one matching child for oneOrMore at '{}'",
ctxt.current_path()
));
valid = false;
continue;
}
_ => {
ctxt.record_error(format!(
"Expected more child elements for sequence at '{}'",
ctxt.current_path()
));
valid = false;
continue;
}
}
}
let child_node = child_nodes[child_idx];
valid &= rng_validate_pattern(child_pat, child_node, schema, ctxt);
child_idx += 1;
}
if child_idx < child_nodes.len() {
let extra_name = get_node_qname(child_nodes[child_idx]);
ctxt.record_error(format!(
"Unexpected extra element '{}' in sequence at '{}'",
extra_name,
ctxt.current_path()
));
valid = false;
}
valid
}
}
fn rng_validate_interleave_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() {
return pattern.children.is_empty();
}
let mut valid = true;
for child_pat in &pattern.children {
let mut child = (*node).children;
let mut matched = false;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let saved_errors = ctxt.errors.len();
let saved_nb = ctxt.nb_errors;
if rng_validate_pattern(child_pat, child, schema, ctxt) {
matched = true;
break;
}
ctxt.errors.truncate(saved_errors);
ctxt.nb_errors = saved_nb;
}
child = (*child).next;
}
if !matched {
match child_pat.pattern_type {
RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
}
_ => {
let pat_desc = format!("{:?}", child_pat.pattern_type);
ctxt.record_error(format!(
"Interleave pattern '{}' did not match any child at '{}'",
pat_desc,
ctxt.current_path()
));
valid = false;
}
}
}
}
valid
}
}
fn rng_validate_zero_or_more(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() || pattern.children.is_empty() {
return true;
}
let child_pat = &pattern.children[0];
let valid = true;
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let saved_errors = ctxt.errors.len();
let saved_nb = ctxt.nb_errors;
if !rng_validate_pattern(child_pat, child, schema, ctxt) {
ctxt.errors.truncate(saved_errors);
ctxt.nb_errors = saved_nb;
break;
}
}
child = (*child).next;
}
valid
}
}
fn rng_validate_one_or_more(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() || pattern.children.is_empty() {
ctxt.record_error(format!(
"Expected at least one matching element for oneOrMore at '{}'",
ctxt.current_path()
));
return false;
}
let child_pat = &pattern.children[0];
let mut matched = false;
let mut valid = true;
let mut child = (*node).children;
while !child.is_null() {
if (*child).type_ == XML_ELEMENT_NODE as c_int {
let saved_errors = ctxt.errors.len();
let saved_nb = ctxt.nb_errors;
if rng_validate_pattern(child_pat, child, schema, ctxt) {
matched = true;
} else {
ctxt.errors.truncate(saved_errors);
ctxt.nb_errors = saved_nb;
break;
}
}
child = (*child).next;
}
if !matched {
ctxt.record_error(format!(
"Expected at least one matching element for oneOrMore at '{}'",
ctxt.current_path()
));
valid = false;
}
valid
}
}
fn rng_validate_optional_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
{
if pattern.children.is_empty() {
return true;
}
let child_pat = &pattern.children[0];
let saved_errors = ctxt.errors.len();
let saved_nb = ctxt.nb_errors;
let result = rng_validate_pattern(child_pat, node, schema, ctxt);
if !result {
ctxt.errors.truncate(saved_errors);
ctxt.nb_errors = saved_nb;
}
true }
}
fn rng_validate_list_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
_schema: &RelaxNgSchema,
_ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() {
return pattern.children.is_empty();
}
let text = get_node_text(node);
if text.trim().is_empty() {
return true;
}
let tokens: Vec<&str> = text.split_whitespace().collect();
let mut valid = true;
for token in &tokens {
if token.is_empty() {
valid = false;
break;
}
}
valid
}
}
fn rng_validate_group_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
{
rng_validate_sequence_pattern(pattern, node, schema, ctxt)
}
}
fn rng_validate_data_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() {
return false;
}
let text = get_node_text(node);
let datatype = pattern.datatype.as_deref();
if !rng_validate_datatype_value(datatype, &text) {
ctxt.record_error(format!(
"Value '{}' does not match datatype '{:?}' at '{}'",
text,
datatype,
ctxt.current_path()
));
return false;
}
true
}
}
fn rng_validate_value_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
unsafe {
if node.is_null() {
return false;
}
let text = get_node_text(node).trim().to_string();
let expected = pattern.value.as_deref().unwrap_or("");
if text != expected {
ctxt.record_error(format!(
"Value '{}' does not match expected value '{}' at '{}'",
text,
expected,
ctxt.current_path()
));
return false;
}
true
}
}
fn rng_validate_ref_pattern(
pattern: &RelaxNgPattern,
node: *mut _xmlNode,
schema: &RelaxNgSchema,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
{
let ref_name = pattern.name.as_deref().unwrap_or("");
if ref_name.is_empty() {
ctxt.record_error("Ref pattern has no name".to_string());
return false;
}
match schema.grammar.lookup(ref_name) {
Some(def_pattern) => {
rng_validate_pattern(def_pattern, node, schema, ctxt)
}
None => {
ctxt.record_error(format!(
"Undefined reference '{}' at '{}'",
ref_name,
ctxt.current_path()
));
false
}
}
}
}
fn rng_validate_datatype_value(datatype: Option<&str>, value: &str) -> bool {
let dt = match datatype {
Some(d) => d,
None => return true, };
match dt {
"string" | "token" => true,
"boolean" => {
matches!(value, "true" | "false" | "1" | "0")
}
"integer" | "int" | "short" | "byte" | "long" => {
if value.is_empty() {
return false;
}
let trimmed = if value.starts_with('+') || value.starts_with('-') {
&value[1..]
} else {
value
};
!trimmed.is_empty() && trimmed.chars().all(|c| c.is_ascii_digit())
}
"decimal" | "double" | "float" => {
if value.is_empty() {
return false;
}
if matches!(dt, "float" | "double") && matches!(value, "INF" | "-INF" | "NaN") {
return true;
}
value.parse::<f64>().is_ok()
}
"NCName" | "Name" | "ID" | "IDREF" | "NMTOKEN" => {
!value.is_empty() && !value.starts_with(|c: char| c.is_ascii_digit())
}
"anyURI" => {
!value.is_empty() && !value.contains(char::is_whitespace)
}
"QName" => {
if value.is_empty() {
return false;
}
if let Some(pos) = value.find(':') {
pos > 0 && pos < value.len() - 1
} else {
true
}
}
_ => {
true
}
}
}
pub fn rng_parse_schema(xml_doc: &str) -> Result<RelaxNgSchema, String> {
rng_parse(xml_doc)
}
pub unsafe fn rng_parse_schema_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
rng_parse_doc(doc)
}
pub unsafe fn rng_validate_doc_schema(
schema: &RelaxNgSchema,
doc: *mut _xmlDoc,
ctxt: &mut RelaxNgValidCtxt,
) -> bool {
rng_validate_doc(schema, doc, ctxt)
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGNewParserCtxt(url: *const c_char) -> *mut c_void {
if url.is_null() {
let ctxt = allocator::xmlMallocZero(size_of::<RelaxNgSchema>() as usize);
return ctxt;
}
let url_str = unsafe {
let mut len = 0;
while *url.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(url as *const u8, len);
String::from_utf8_lossy(slice).to_string()
};
if !url_str.is_empty() {
let url_c = std::ffi::CString::new(url_str.clone()).ok();
if let Some(c) = url_c {
let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
if !doc.is_null() {
let result = rng_parse_doc(doc);
crate::abi::exports_xml2::xmlFreeDoc(doc);
if let Ok(schema) = result {
let schema_box = Box::new(schema);
return Box::into_raw(schema_box) as *mut c_void;
}
}
}
}
allocator::xmlMallocZero(size_of::<RelaxNgSchema>() as usize)
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGNewMemParserCtxt(
buffer: *const c_char,
size: c_int,
) -> *mut c_void {
if buffer.is_null() || size <= 0 {
return ptr::null_mut();
}
let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
let xml_str = String::from_utf8_lossy(buf_slice).to_string();
match rng_parse(&xml_str) {
Ok(schema) => {
let schema_box = Box::new(schema);
Box::into_raw(schema_box) as *mut c_void
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub const unsafe extern "C" fn xmlRelaxNGParse(ctxt: *mut c_void) -> *mut c_void {
if ctxt.is_null() {
return ptr::null_mut();
}
ctxt
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGFree(schema: *mut c_void) {
if schema.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(schema as *mut RelaxNgSchema);
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGFreeParserCtxt(ctxt: *mut c_void) {
if ctxt.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(ctxt as *mut RelaxNgSchema);
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGNewValidCtxt(schema: *mut c_void) -> *mut c_void {
let mut ctxt = RelaxNgValidCtxt::new();
if !schema.is_null() {
unsafe {
let schema_ref = &*(schema as *const RelaxNgSchema);
ctxt.schema = Some(schema_ref.clone());
}
}
let boxed = Box::new(ctxt);
Box::into_raw(boxed) as *mut c_void
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGFreeValidCtxt(ctxt: *mut c_void) {
if ctxt.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(ctxt as *mut RelaxNgValidCtxt);
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
if ctxt.is_null() || doc.is_null() {
return -1;
}
unsafe {
let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
let schema = match &valid_ctxt.schema {
Some(s) => s,
None => return -1,
};
let mut temp_ctxt = RelaxNgValidCtxt::new();
let valid = rng_validate_doc(schema, doc, &mut temp_ctxt);
if valid {
0
} else {
valid_ctxt.errors = temp_ctxt.errors;
valid_ctxt.nb_errors = temp_ctxt.nb_errors;
temp_ctxt.nb_errors
}
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlRelaxNGValidateFullElement(
ctxt: *mut c_void,
doc: *mut _xmlDoc,
elem: *mut _xmlNode,
) -> c_int {
if ctxt.is_null() || doc.is_null() || elem.is_null() {
return -1;
}
unsafe {
let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
let schema = match &valid_ctxt.schema {
Some(s) => s,
None => return -1,
};
let mut temp_ctxt = RelaxNgValidCtxt::new();
temp_ctxt.path = valid_ctxt.path.clone();
let start_pattern = match &schema.grammar.start {
Some(p) => p,
None => return -1,
};
let valid = rng_validate_pattern(start_pattern, elem, schema, &mut temp_ctxt);
if valid {
0
} else {
valid_ctxt.errors = temp_ctxt.errors;
valid_ctxt.nb_errors = temp_ctxt.nb_errors;
temp_ctxt.nb_errors
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_name_class_name() {
let nc = RelaxNgNameClass::Name("foo".to_string());
assert!(nc.matches("foo", None));
assert!(!nc.matches("bar", None));
assert!(!nc.matches("FOO", None));
}
#[test]
fn test_name_class_any_name() {
let nc = RelaxNgNameClass::AnyName;
assert!(nc.matches("foo", None));
assert!(nc.matches("bar", None));
assert!(nc.matches("anything", Some("urn:ns")));
}
#[test]
fn test_name_class_ns_name() {
let nc = RelaxNgNameClass::NsName("urn:example".to_string());
assert!(nc.matches("foo", Some("urn:example")));
assert!(!nc.matches("foo", Some("urn:other")));
assert!(!nc.matches("foo", None));
}
#[test]
fn test_name_class_choice() {
let nc = RelaxNgNameClass::Choice(vec![
RelaxNgNameClass::Name("a".to_string()),
RelaxNgNameClass::Name("b".to_string()),
]);
assert!(nc.matches("a", None));
assert!(nc.matches("b", None));
assert!(!nc.matches("c", None));
}
#[test]
fn test_name_class_except() {
let nc = RelaxNgNameClass::Except(
Box::new(RelaxNgNameClass::AnyName),
Box::new(RelaxNgNameClass::Name("bad".to_string())),
);
assert!(nc.matches("good", None));
assert!(!nc.matches("bad", None));
}
#[test]
fn test_parse_simple_element_schema() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<text/>
</element>"#;
let result = rng_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
let schema = result.unwrap();
assert!(schema.grammar.start.is_some());
if let Some(ref start) = schema.grammar.start {
assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
assert_eq!(start.name.as_deref(), Some("root"));
}
}
#[test]
fn test_parse_grammar_schema() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<element name="root">
<text/>
</element>
</start>
</grammar>"#;
let result = rng_parse(schema_xml);
assert!(
result.is_ok(),
"Failed to parse grammar: {:?}",
result.err()
);
let schema = result.unwrap();
assert!(schema.grammar.start.is_some());
}
#[test]
fn test_parse_with_define_and_ref() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<define name="textBlock">
<text/>
</define>
<start>
<element name="doc">
<ref name="textBlock"/>
</element>
</start>
</grammar>"#;
let result = rng_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let schema = result.unwrap();
assert_eq!(schema.grammar.defines.len(), 1);
assert_eq!(schema.grammar.defines[0].name, "textBlock");
assert!(schema.grammar.start.is_some());
}
#[test]
fn test_parse_choice_schema() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<choice>
<element name="a">
<text/>
</element>
<element name="b">
<text/>
</element>
</choice>
</start>
</grammar>"#;
let result = rng_parse(schema_xml);
assert!(result.is_ok(), "Failed to parse choice: {:?}", result.err());
}
#[test]
fn test_parse_attribute_schema() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<attribute name="attr1">
<text/>
</attribute>
<text/>
</element>"#;
let result = rng_parse(schema_xml);
assert!(
result.is_ok(),
"Failed to parse attribute: {:?}",
result.err()
);
}
#[test]
fn test_parse_empty_document_fails() {
let result = rng_parse("");
assert!(result.is_err());
}
#[test]
fn test_parse_invalid_xml_fails() {
let result = rng_parse("not valid xml <<<");
assert!(result.is_err());
}
#[test]
fn test_validate_simple_element() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null(), "Failed to parse document");
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_element_mismatch() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="expected" xmlns="http://relaxng.org/ns/structure/1.0">
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<actual>Content</actual>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid, "Validation should have failed");
assert!(ctxt.nb_errors > 0);
}
#[test]
fn test_validate_with_attribute() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<attribute name="id">
<text/>
</attribute>
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root id="x1">Content</root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_missing_attribute() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<attribute name="required">
<text/>
</attribute>
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Content</root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(
!valid,
"Validation should have failed for missing attribute"
);
}
#[test]
fn test_validate_with_choice() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<choice>
<element name="a">
<text/>
</element>
<element name="b">
<text/>
</element>
</choice>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<a>First choice</a>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Choice validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_choice_no_match() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<choice>
<element name="a">
<text/>
</element>
<element name="b">
<text/>
</element>
</choice>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<c>Neither choice</c>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(
!valid,
"Validation should have failed for no matching choice"
);
}
#[test]
fn test_validate_grammar_with_ref() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<define name="para">
<element name="p">
<text/>
</element>
</define>
<start>
<element name="doc">
<zeroOrMore>
<ref name="para"/>
</zeroOrMore>
</element>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<doc><p>First</p><p>Second</p></doc>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Ref validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_zero_or_more() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<zeroOrMore>
<element name="item">
<text/>
</element>
</zeroOrMore>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><item>A</item><item>B</item></root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "zeroOrMore validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_zero_or_more_empty() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<zeroOrMore>
<element name="item">
<text/>
</element>
</zeroOrMore>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root></root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Empty zeroOrMore should be valid");
}
#[test]
fn test_validate_one_or_more() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<oneOrMore>
<element name="item">
<text/>
</element>
</oneOrMore>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><item>Single</item></root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "oneOrMore validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_optional_present() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<optional>
<element name="opt">
<text/>
</element>
</optional>
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><opt>present</opt>text</root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(
valid,
"Optional present validation failed: {:?}",
ctxt.errors
);
}
#[test]
fn test_validate_optional_absent() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<optional>
<element name="opt">
<text/>
</element>
</optional>
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>text only</root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(
valid,
"Optional absent validation failed: {:?}",
ctxt.errors
);
}
#[test]
fn test_validate_sequence() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<element name="root">
<sequence>
<element name="first">
<text/>
</element>
<element name="second">
<text/>
</element>
</sequence>
</element>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><first>First</first><second>Second</second></root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Sequence validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_data_pattern() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
<data type="integer"/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<age>25</age>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Data pattern validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_data_pattern_invalid() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
<data type="integer"/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<age>not-a-number</age>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid, "Validation should have failed for invalid integer");
}
#[test]
fn test_validate_value_pattern() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
<value>active</value>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<status>active</status>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Value pattern validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_value_pattern_mismatch() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
<value>active</value>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<status>inactive</status>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid, "Validation should have failed for value mismatch");
}
#[test]
fn test_validate_not_allowed() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<notAllowed/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>should not be allowed</root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid, "notAllowed should cause validation failure");
}
#[test]
fn test_validate_interleave() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<element name="root">
<interleave>
<element name="a">
<text/>
</element>
<element name="b">
<text/>
</element>
</interleave>
</element>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><a>A</a><b>B</b></root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Interleave validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_empty_element() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="br" xmlns="http://relaxng.org/ns/structure/1.0">
<empty/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<br/>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Empty element validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_datatype_string() {
assert!(rng_validate_datatype_value(Some("string"), "hello"));
assert!(rng_validate_datatype_value(Some("string"), ""));
}
#[test]
fn test_validate_datatype_boolean() {
assert!(rng_validate_datatype_value(Some("boolean"), "true"));
assert!(rng_validate_datatype_value(Some("boolean"), "false"));
assert!(rng_validate_datatype_value(Some("boolean"), "1"));
assert!(rng_validate_datatype_value(Some("boolean"), "0"));
assert!(!rng_validate_datatype_value(Some("boolean"), "yes"));
assert!(!rng_validate_datatype_value(Some("boolean"), "no"));
}
#[test]
fn test_validate_datatype_integer() {
assert!(rng_validate_datatype_value(Some("integer"), "42"));
assert!(rng_validate_datatype_value(Some("integer"), "-42"));
assert!(rng_validate_datatype_value(Some("integer"), "+42"));
assert!(!rng_validate_datatype_value(Some("integer"), "12.5"));
assert!(!rng_validate_datatype_value(Some("integer"), "abc"));
assert!(!rng_validate_datatype_value(Some("integer"), ""));
}
#[test]
fn test_validate_datatype_decimal() {
assert!(rng_validate_datatype_value(Some("decimal"), "42"));
assert!(rng_validate_datatype_value(Some("decimal"), "12.5"));
assert!(rng_validate_datatype_value(Some("decimal"), "-3.14"));
assert!(!rng_validate_datatype_value(Some("decimal"), ""));
}
#[test]
fn test_validate_datatype_float() {
assert!(rng_validate_datatype_value(Some("float"), "3.14"));
assert!(rng_validate_datatype_value(Some("float"), "INF"));
assert!(rng_validate_datatype_value(Some("float"), "-INF"));
assert!(rng_validate_datatype_value(Some("float"), "NaN"));
assert!(!rng_validate_datatype_value(Some("float"), ""));
}
#[test]
fn test_validate_datatype_ncname() {
assert!(rng_validate_datatype_value(Some("NCName"), "myElement"));
assert!(rng_validate_datatype_value(Some("NCName"), "_foo"));
assert!(!rng_validate_datatype_value(Some("NCName"), "123abc"));
assert!(!rng_validate_datatype_value(Some("NCName"), ""));
}
#[test]
fn test_validate_datatype_any_uri() {
assert!(rng_validate_datatype_value(
Some("anyURI"),
"http://example.com"
));
assert!(rng_validate_datatype_value(Some("anyURI"), "urn:isbn:1234"));
assert!(!rng_validate_datatype_value(Some("anyURI"), ""));
assert!(!rng_validate_datatype_value(Some("anyURI"), "has space"));
}
#[test]
fn test_validate_datatype_qname() {
assert!(rng_validate_datatype_value(Some("QName"), "ns:local"));
assert!(rng_validate_datatype_value(Some("QName"), "local"));
assert!(!rng_validate_datatype_value(Some("QName"), ""));
}
#[test]
fn test_c_abi_new_parse_free() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<text/>
</element>"#;
let ctxt = unsafe {
xmlRelaxNGNewMemParserCtxt(
schema_xml.as_ptr() as *const c_char,
schema_xml.len() as c_int,
)
};
assert!(!ctxt.is_null(), "Parser context should not be null");
let schema = unsafe { xmlRelaxNGParse(ctxt) };
assert!(!schema.is_null(), "Schema should not be null");
unsafe { xmlRelaxNGFree(schema) };
}
#[test]
fn test_c_abi_validate_doc() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Hello</root>"#;
let ctxt = unsafe {
xmlRelaxNGNewMemParserCtxt(
schema_xml.as_ptr() as *const c_char,
schema_xml.len() as c_int,
)
};
let schema = unsafe { xmlRelaxNGParse(ctxt) };
assert!(!schema.is_null());
let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
assert!(!valid_ctxt.is_null());
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let result = unsafe { xmlRelaxNGValidateDoc(valid_ctxt, doc) };
assert_eq!(result, 0, "Validation should succeed");
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
unsafe { xmlRelaxNGFree(schema) };
}
#[test]
fn test_c_abi_validate_full_element() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="item" xmlns="http://relaxng.org/ns/structure/1.0">
<text/>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><item>Content</item></root>"#;
let ctxt = unsafe {
xmlRelaxNGNewMemParserCtxt(
schema_xml.as_ptr() as *const c_char,
schema_xml.len() as c_int,
)
};
let schema = unsafe { xmlRelaxNGParse(ctxt) };
assert!(!schema.is_null());
let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
assert!(!valid_ctxt.is_null());
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let item = unsafe {
let mut node = (*doc).children;
while !node.is_null() {
if (*node).type_ == XML_ELEMENT_NODE as c_int {
break;
}
node = (*node).next;
}
if !node.is_null() {
node = (*node).children;
while !node.is_null() {
if (*node).type_ == XML_ELEMENT_NODE as c_int {
break;
}
node = (*node).next;
}
}
node
};
assert!(!item.is_null(), "Should find <item> element");
let result = unsafe { xmlRelaxNGValidateFullElement(valid_ctxt, doc, item) };
assert_eq!(result, 0, "Element validation should succeed");
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
unsafe { xmlRelaxNGFree(schema) };
}
#[test]
fn test_c_abi_null_handling() {
assert_eq!(
unsafe { xmlRelaxNGValidateDoc(ptr::null_mut(), ptr::null_mut()) },
-1
);
assert_eq!(
unsafe {
xmlRelaxNGValidateFullElement(ptr::null_mut(), ptr::null_mut(), ptr::null_mut())
},
-1
);
assert!(unsafe { xmlRelaxNGNewMemParserCtxt(ptr::null(), 0).is_null() });
unsafe { xmlRelaxNGFree(ptr::null_mut()) };
unsafe { xmlRelaxNGFreeParserCtxt(ptr::null_mut()) };
unsafe { xmlRelaxNGFreeValidCtxt(ptr::null_mut()) };
}
#[test]
fn test_parse_with_div() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<div>
<define name="shared">
<text/>
</define>
</div>
<start>
<element name="root">
<ref name="shared"/>
</element>
</start>
</grammar>"#;
let result = rng_parse(schema_xml);
assert!(
result.is_ok(),
"Failed to parse with div: {:?}",
result.err()
);
let schema = result.unwrap();
assert_eq!(schema.grammar.defines.len(), 1);
assert_eq!(schema.grammar.defines[0].name, "shared");
}
#[test]
fn test_validate_list_pattern() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="tokens" xmlns="http://relaxng.org/ns/structure/1.0">
<list>
<data type="token"/>
</list>
</element>"#;
let doc_xml = r#"<?xml version="1.0"?>
<tokens>abc def ghi</tokens>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "List pattern validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_group_pattern() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<element name="root">
<group>
<element name="a">
<text/>
</element>
<element name="b">
<text/>
</element>
</group>
</element>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><a>First</a><b>Second</b></root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(valid, "Group validation failed: {:?}", ctxt.errors);
}
#[test]
fn test_validate_undefined_ref() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<element name="root">
<ref name="undefined"/>
</element>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root>Content</root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid, "Undefined ref should cause failure");
}
#[test]
fn test_validate_null_doc() {
let schema = RelaxNgSchema::new();
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
assert!(!valid);
}
#[test]
fn test_parse_external_ref_schema() {
let schema_xml = r#"<?xml version="1.0"?>
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<externalRef href="external.rng"/>
</element>"#;
let result = rng_parse(schema_xml);
assert!(
result.is_ok(),
"Failed to parse externalRef: {:?}",
result.err()
);
let schema = result.unwrap();
if let Some(ref start) = schema.grammar.start {
assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
assert_eq!(start.name.as_deref(), Some("root"));
}
}
#[test]
fn test_validate_boolean_datatype() {
assert!(rng_validate_datatype_value(Some("boolean"), "true"));
assert!(rng_validate_datatype_value(Some("boolean"), "false"));
assert!(!rng_validate_datatype_value(Some("boolean"), "maybe"));
}
#[test]
fn test_validate_unknown_datatype() {
assert!(rng_validate_datatype_value(Some("custom-type"), "anything"));
}
#[test]
fn test_validate_no_datatype() {
assert!(rng_validate_datatype_value(None, "anything"));
}
#[test]
fn test_parse_schema_with_ns_prefix() {
let schema_xml = r#"<?xml version="1.0"?>
<rng:element name="root" xmlns:rng="http://relaxng.org/ns/structure/1.0">
<rng:text/>
</rng:element>"#;
let result = rng_parse(schema_xml);
assert!(result.is_ok(), "Failed with ns prefix: {:?}", result.err());
}
#[test]
fn test_validation_context_path() {
let mut ctxt = RelaxNgValidCtxt::new();
assert_eq!(ctxt.current_path(), "/");
ctxt.path.push("root".to_string());
assert_eq!(ctxt.current_path(), "/root");
ctxt.path.push("child".to_string());
assert_eq!(ctxt.current_path(), "/root/child");
ctxt.path.pop();
assert_eq!(ctxt.current_path(), "/root");
}
#[test]
fn test_validate_sequence_wrong_order() {
let schema_xml = r#"<?xml version="1.0"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<element name="root">
<sequence>
<element name="first">
<text/>
</element>
<element name="second">
<text/>
</element>
</sequence>
</element>
</start>
</grammar>"#;
let doc_xml = r#"<?xml version="1.0"?>
<root><second>Wrong</second><first>Order</first></root>"#;
let schema = rng_parse(schema_xml).expect("Failed to parse schema");
let doc = unsafe {
crate::abi::exports_xml2::xmlReadMemory(
doc_xml.as_ptr() as *const c_char,
doc_xml.len() as c_int,
c"test.xml".as_ptr() as *const c_char,
ptr::null(),
0,
)
};
assert!(!doc.is_null());
let mut ctxt = RelaxNgValidCtxt::new();
let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
assert!(!valid, "Wrong sequence order should fail");
}
}