use crate::schema::types::{
AttributeDef, CompiledSchema, ComplexType, ContentModel, SimpleType, TypeDef,
};
use crate::schema::xsd::facets::{FacetCache, FacetConstraints, FacetValidator};
use crate::schema::xsd::primitive::PrimitiveKind;
pub(crate) struct CollectedAttrs {
pub defs: Vec<AttributeDef>,
pub wildcard: Option<crate::schema::types::WildcardConstraint>,
pub is_any_type: bool,
}
impl CollectedAttrs {
pub(crate) fn collect(schema: &CompiledSchema, complex: &ComplexType) -> Self {
Self {
defs: collect_attributes(schema, complex)
.into_iter()
.cloned()
.collect(),
wildcard: collect_attr_wildcard(schema, complex).cloned(),
is_any_type: complex.name == "anyType",
}
}
}
pub(crate) fn collect_attributes<'a>(
schema: &'a CompiledSchema,
complex: &'a ComplexType,
) -> Vec<&'a AttributeDef> {
let mut out: Vec<&AttributeDef> = Vec::new();
let mut current = complex;
for _ in 0..16 {
for attr in ¤t.attributes {
if !out.iter().any(|a| a.name == attr.name) {
out.push(attr);
}
}
match schema.complex_base_def(current) {
Some(TypeDef::Complex(c)) => current = c,
_ => break,
}
}
out
}
fn collect_attr_wildcard<'a>(
schema: &'a CompiledSchema,
complex: &'a ComplexType,
) -> Option<&'a crate::schema::types::WildcardConstraint> {
let mut current = complex;
for _ in 0..16 {
if let Some(ref w) = current.attr_wildcard {
return Some(w);
}
match schema.complex_base_def(current) {
Some(TypeDef::Complex(c)) => current = c,
_ => break,
}
}
None
}
fn resolve_ref<'a>(schema: &'a CompiledSchema, attr: &'a AttributeDef) -> &'a AttributeDef {
if !attr.is_ref {
return attr;
}
if let Some(rn) = &attr.ref_ns
&& let Some(global) = schema.attribute_ns(&rn.namespace_uri, &rn.local_name)
{
return global;
}
if let Some((_, found)) = schema
.attributes_ns
.iter()
.find(|(k, _)| *k.local_name == *attr.name)
{
return found;
}
attr
}
fn attribute_simple_type<'a>(
schema: &'a CompiledSchema,
attr: &'a AttributeDef,
) -> Option<&'a SimpleType> {
if let Some(ref inline) = attr.inline_type {
return Some(inline);
}
let type_ref = attr.type_ref.as_deref()?;
match schema.type_by_ref(attr.type_ns.as_ref(), type_ref) {
Some(TypeDef::Simple(s)) => Some(s),
_ => None,
}
}
pub(crate) fn attribute_primitive_kind(
schema: &CompiledSchema,
complex: &ComplexType,
attr_local: &str,
) -> Option<PrimitiveKind> {
let def = collect_attributes(schema, complex)
.into_iter()
.find(|d| d.name == attr_local || d.name.rsplit(':').next() == Some(attr_local))?;
let def = resolve_ref(schema, def);
let simple = attribute_simple_type(schema, def)?;
PrimitiveKind::resolve(schema, simple)
}
pub(crate) fn element_text_primitive_kind(
schema: &CompiledSchema,
type_def: &TypeDef,
) -> Option<PrimitiveKind> {
match type_def {
TypeDef::Simple(simple) => PrimitiveKind::resolve(schema, simple),
TypeDef::Complex(complex) => {
if matches!(&complex.content, ContentModel::SimpleContent { .. })
&& let Some(TypeDef::Simple(simple)) = schema.complex_base_def(complex)
{
return PrimitiveKind::resolve(schema, simple);
}
None
}
}
}
pub(crate) fn validate_attribute_value(
schema: &CompiledSchema,
attr: &AttributeDef,
value: &str,
cache: &mut FacetCache,
) -> Option<String> {
let attr = resolve_ref(schema, attr);
if let Some(ref fixed) = attr.fixed {
if value != fixed {
return Some(format!(
"attribute '{}' must have the fixed value '{}', found '{}'",
attr.name, fixed, value
));
}
}
let simple = attribute_simple_type(schema, attr)?;
let constraints = cache.get(schema, simple);
let validator = FacetValidator::new(&constraints);
if let Err(e) = validator.validate(value) {
return Some(format!("attribute '{}': {}", attr.name, e));
}
if let Some(kind) = PrimitiveKind::resolve(schema, simple) {
if let Err(e) = kind.validate(value) {
return Some(format!("attribute '{}': {}", attr.name, e));
}
}
None
}
pub(crate) fn is_exempt_attribute(name: &str) -> bool {
name == "xmlns"
|| name.starts_with("xmlns:")
|| name.starts_with("xsi:")
|| name.contains("schemaLocation")
}
#[derive(Default)]
pub(crate) struct AttrValidation {
pub errors: Vec<String>,
pub ids: Vec<String>,
pub idrefs: Vec<String>,
}
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
pub(crate) fn validate_element_attributes<'a>(
schema: &CompiledSchema,
collected: &CollectedAttrs,
attributes: impl Iterator<Item = (&'a str, Option<&'a str>, &'a str)> + Clone,
cache: &mut FacetCache,
) -> AttrValidation {
let defs = &collected.defs;
let wildcard = collected.wildcard.as_ref();
let mut out = AttrValidation::default();
if collected.is_any_type {
return out;
}
for (name, ns, value) in attributes.clone() {
if is_exempt_attribute(name) || ns == Some(XML_NS) {
continue;
}
let local = name.rsplit(':').next().unwrap_or(name);
if let Some(def) = defs.iter().find(|d| d.name == local || d.name == name) {
if let Some(msg) = validate_attribute_value(schema, def, value, cache) {
out.errors.push(msg);
}
collect_id_values(schema, resolve_ref(schema, def), value, &mut out, cache);
continue;
}
match wildcard {
Some(w) if w.matches(ns) => match w.process_contents {
crate::schema::types::ProcessContents::Skip => {}
crate::schema::types::ProcessContents::Lax
| crate::schema::types::ProcessContents::Strict => {
let global = schema.attribute_ns(ns.unwrap_or(""), local).or_else(|| {
schema
.attributes_ns
.iter()
.find(|(k, _)| *k.local_name == *local)
.map(|(_, a)| a)
});
match global {
Some(def) => {
if let Some(msg) = validate_attribute_value(schema, def, value, cache) {
out.errors.push(msg);
}
collect_id_values(schema, def, value, &mut out, cache);
}
None if w.process_contents
== crate::schema::types::ProcessContents::Strict =>
{
out.errors.push(format!(
"attribute '{}' matched a strict wildcard but is not declared",
name
));
}
None => {}
}
}
},
_ => {
out.errors
.push(format!("attribute '{}' is not allowed", name));
}
}
}
for def in defs {
if def.required
&& !attributes
.clone()
.any(|(n, _, _)| n.rsplit(':').next().unwrap_or(n) == def.name || n == def.name)
{
out.errors
.push(format!("required attribute '{}' is missing", def.name));
}
}
out
}
fn collect_id_values(
schema: &CompiledSchema,
attr: &AttributeDef,
value: &str,
out: &mut AttrValidation,
cache: &mut FacetCache,
) {
let Some(simple) = attribute_simple_type(schema, attr) else {
let kind = attr
.type_ref
.as_deref()
.and_then(PrimitiveKind::from_type_name);
push_id_values(kind, None, false, value, out);
return;
};
let constraints = cache.get(schema, simple);
push_id_values_from_constraints(&constraints, value, out);
}
pub(crate) fn push_id_values_from_constraints(
constraints: &FacetConstraints,
value: &str,
out: &mut AttrValidation,
) {
push_id_values(
constraints.value_kind,
constraints.item_kind,
constraints.is_list,
value,
out,
);
}
fn push_id_values(
kind: Option<PrimitiveKind>,
item_kind: Option<PrimitiveKind>,
is_list: bool,
value: &str,
out: &mut AttrValidation,
) {
if is_list {
match item_kind {
Some(k) if k.is_idref() => out
.idrefs
.extend(value.split_whitespace().map(str::to_string)),
Some(k) if k.is_id() => out.ids.extend(value.split_whitespace().map(str::to_string)),
_ => {}
}
return;
}
let trimmed = value.trim();
if trimmed.is_empty() {
return; }
match kind {
Some(k) if k.is_id() => out.ids.push(trimmed.to_string()),
Some(k) if k.is_idref() => out.idrefs.push(trimmed.to_string()),
_ => {}
}
}