use oxml::{Document, NodeId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compositor {
Sequence,
Choice,
All,
}
#[derive(Debug, Clone)]
pub enum Term {
Element {
name: String,
type_name: Option<String>,
},
Wildcard(String),
Group {
compositor: Compositor,
particles: Vec<Particle>,
},
}
#[derive(Debug, Clone)]
pub struct Particle {
pub min: usize,
pub max: Option<usize>,
pub term: Term,
}
impl Particle {
#[must_use]
pub fn collapsed(&self) -> &Self {
match &self.term {
Term::Group { particles, .. }
if particles.len() == 1
&& self.min == 1
&& self.max == Some(1) =>
{
particles[0].collapsed()
}
_ => self,
}
}
#[must_use]
pub fn effective_total_range(&self) -> (usize, Option<usize>) {
let (min, max) = match &self.term {
Term::Element { .. } | Term::Wildcard(_) => (1, Some(1)),
Term::Group {
compositor,
particles,
} => {
let inner: Vec<(usize, Option<usize>)> = particles
.iter()
.map(Particle::effective_total_range)
.collect();
let maxes = |combine: fn(usize, usize) -> usize| {
inner.iter().try_fold(0usize, |acc, (_, m)| {
m.map(|m| combine(acc, m))
})
};
match compositor {
Compositor::Choice => (
inner.iter().map(|(m, _)| *m).min().unwrap_or(0),
maxes(usize::max),
),
Compositor::Sequence | Compositor::All => (
inner.iter().map(|(m, _)| *m).sum(),
maxes(usize::saturating_add),
),
}
}
};
(
min.saturating_mul(self.min),
match (max, self.max) {
(Some(a), Some(b)) => Some(a.saturating_mul(b)),
_ => None,
},
)
}
#[must_use]
pub fn emptiable(&self) -> bool {
if self.min == 0 {
return true;
}
match &self.term {
Term::Element { .. } | Term::Wildcard(_) => false,
Term::Group {
compositor,
particles,
} => match compositor {
Compositor::Choice => {
particles.is_empty()
|| particles.iter().any(Particle::emptiable)
}
Compositor::Sequence | Compositor::All => {
particles.iter().all(Particle::emptiable)
}
},
}
}
}
#[must_use]
pub fn is_valid_restriction(
derived: &Particle,
base: &Particle,
type_derives: &dyn Fn(&str, &str) -> bool,
) -> bool {
let derived = derived.collapsed();
let base = base.collapsed();
match (&derived.term, &base.term) {
(
Term::Element {
name: r,
type_name: rt,
},
Term::Element {
name: b,
type_name: bt,
},
) => {
r == b
&& occurrence_ok(derived, base)
&& match (rt.as_deref(), bt.as_deref()) {
(Some(r), Some(b)) => r == b || type_derives(r, b),
_ => true,
}
}
(Term::Element { .. }, Term::Wildcard(_)) => {
occurrence_ok(derived, base)
}
(Term::Wildcard(r), Term::Wildcard(b)) => {
occurrence_ok(derived, base) && namespace_subset(r, b)
}
(Term::Group { particles, .. }, Term::Wildcard(namespace)) => {
let unbounded = Particle {
min: 0,
max: None,
term: Term::Wildcard(namespace.clone()),
};
if !particles
.iter()
.all(|p| is_valid_restriction(p, &unbounded, type_derives))
{
return false;
}
let (min, max) = derived.effective_total_range();
occurrence_ok(
&Particle {
min,
max,
term: Term::Wildcard(String::new()),
},
base,
)
}
(Term::Wildcard(_), Term::Element { .. } | Term::Group { .. }) => false,
(
Term::Group {
particles,
compositor: _,
},
Term::Element { .. },
) => match particles.as_slice() {
[only] if derived.min == 1 && derived.max == Some(1) => {
is_valid_restriction(only, base, type_derives)
}
_ => false,
},
(
Term::Element { .. },
Term::Group {
compositor,
particles,
},
) => {
match compositor {
Compositor::Choice => particles
.iter()
.any(|b| is_valid_restriction(derived, b, type_derives)),
Compositor::Sequence | Compositor::All => {
particles.iter().enumerate().any(|(i, b)| {
is_valid_restriction(derived, b, type_derives)
&& particles
.iter()
.enumerate()
.all(|(j, other)| j == i || other.emptiable())
})
}
}
}
(
Term::Group {
compositor: rc,
particles: rp,
},
Term::Group {
compositor: bc,
particles: bp,
},
) => {
occurrence_ok(derived, base)
&& groups_match(*rc, rp, *bc, bp, type_derives)
}
}
}
fn groups_match(
rc: Compositor,
rp: &[Particle],
bc: Compositor,
bp: &[Particle],
type_derives: &dyn Fn(&str, &str) -> bool,
) -> bool {
match (rc, bc) {
(Compositor::Sequence | Compositor::All, Compositor::All) => {
map_unordered(rp, bp, type_derives)
}
(Compositor::Sequence | Compositor::All, Compositor::Sequence) => {
map_in_order(rp, bp, true, type_derives)
}
(Compositor::Choice, Compositor::Choice) => {
map_in_order(rp, bp, false, type_derives)
}
(Compositor::Sequence | Compositor::All, Compositor::Choice) => {
rp.iter().all(|r| {
bp.iter().any(|b| is_valid_restriction(r, b, type_derives))
})
}
(Compositor::Choice, _) => false,
}
}
fn occurrence_ok(derived: &Particle, base: &Particle) -> bool {
if derived.min < base.min {
return false;
}
match (derived.max, base.max) {
(_, None) => true,
(None, Some(_)) => false,
(Some(r), Some(b)) => r <= b,
}
}
fn map_in_order(
derived: &[Particle],
base: &[Particle],
skipped_must_be_emptiable: bool,
type_derives: &dyn Fn(&str, &str) -> bool,
) -> bool {
let mut at = 0usize;
for r in derived {
let mut found = None;
for (offset, b) in base[at..].iter().enumerate() {
if is_valid_restriction(r, b, type_derives) {
found = Some(at + offset);
break;
}
if skipped_must_be_emptiable && !b.emptiable() {
return false;
}
}
let Some(index) = found else {
return false;
};
at = index + 1;
}
!skipped_must_be_emptiable || base[at..].iter().all(Particle::emptiable)
}
fn map_unordered(
derived: &[Particle],
base: &[Particle],
type_derives: &dyn Fn(&str, &str) -> bool,
) -> bool {
let mut used = vec![false; base.len()];
for r in derived {
let Some(index) = base.iter().enumerate().position(|(i, b)| {
!used[i] && is_valid_restriction(r, b, type_derives)
}) else {
return false;
};
used[index] = true;
}
base.iter()
.zip(&used)
.all(|(b, taken)| *taken || b.emptiable())
}
#[must_use]
pub fn namespace_subset(derived: &str, base: &str) -> bool {
if base == "##any" {
return true;
}
if derived == "##any" {
return false;
}
if base == derived {
return true;
}
if base == "##other" || derived == "##other" {
return true;
}
let allowed: Vec<&str> = base.split_whitespace().collect();
derived.split_whitespace().all(|n| allowed.contains(&n))
}
#[must_use]
pub fn particle_of(
doc: &Document,
host: NodeId,
groups: &dyn Fn(&str) -> Option<NodeId>,
depth: usize,
) -> Option<Particle> {
if depth > 32 {
return None;
}
let local = |id: NodeId| doc.element_name(id).map(|n| n.local.clone());
let name = local(host)?;
let compositor = match name.as_str() {
"sequence" => Compositor::Sequence,
"choice" => Compositor::Choice,
"all" => Compositor::All,
"group" => {
let target = doc
.attribute(host, "ref")
.and_then(|r| groups(r.rsplit(':').next().unwrap_or(r)))?;
let inner =
["sequence", "choice", "all"].into_iter().find_map(|g| {
doc.children(target)
.iter()
.copied()
.find(|&c| local(c).is_some_and(|n| n == g))
})?;
let mut particle = particle_of(doc, inner, groups, depth + 1)?;
particle.min = min_occurs(doc, host);
particle.max = max_occurs(doc, host);
return Some(particle);
}
"element" => {
return Some(Particle {
min: min_occurs(doc, host),
max: max_occurs(doc, host),
term: Term::Element {
name: doc
.attribute(host, "name")
.or_else(|| doc.attribute(host, "ref"))
.unwrap_or_default()
.rsplit(':')
.next()
.unwrap_or_default()
.to_owned(),
type_name: doc
.attribute(host, "type")
.map(|t| t.rsplit(':').next().unwrap_or(t).to_owned()),
},
});
}
"any" => {
return Some(Particle {
min: min_occurs(doc, host),
max: max_occurs(doc, host),
term: Term::Wildcard(
doc.attribute(host, "namespace")
.unwrap_or("##any")
.to_owned(),
),
});
}
_ => return None,
};
let particles = doc
.children(host)
.iter()
.copied()
.filter_map(|c| particle_of(doc, c, groups, depth + 1))
.collect();
Some(Particle {
min: min_occurs(doc, host),
max: max_occurs(doc, host),
term: Term::Group {
compositor,
particles,
},
})
}
fn min_occurs(doc: &Document, id: NodeId) -> usize {
doc.attribute(id, "minOccurs")
.and_then(|v| v.parse().ok())
.unwrap_or(1)
}
fn max_occurs(doc: &Document, id: NodeId) -> Option<usize> {
match doc.attribute(id, "maxOccurs") {
Some("unbounded") => None,
Some(v) => v.parse().ok().or(Some(1)),
None => Some(1),
}
}