Particle Valid (Restriction) derives its pairwise check over the spec's
normalized content model: pointless particles are eliminated (maxOccurs=0
members dropped, a {1,1} single-member group unwrapped, a nested
same-compositor {1,1} group spliced), all language-preserving. The outer
occurrence-range check was wrongly applied to the cross-kind pairings, where
it compares a derived particle's own range against a base group's or
wildcard's. It is now applied only to the same-kind pairings (element/element,
wildcard/wildcard, group/group) and element/wildcard; element/group
(RecurseAsIfGroup), group/element, and group/wildcard match occurrence inside
the case, against a member or as an effective total range.
A group restricting a wildcard (NSRecurseCheckCardinality) is valid when every
leaf is admitted and the effective total occurrence range is within the
wildcard's; a group restricting a single element is bounded the same way, by
that element's name, type derivation, and occurrence range.
XSTS: valid schemas rejected 45 to 32, invalid schemas accepted 528 to 526,
no other bucket raised.
360 lines
20 KiB
Swift
360 lines
20 KiB
Swift
extension PureXML.Schema {
|
|
/// XSD 1.0 "Particle Valid (Restriction)" for the common cases: a complex
|
|
/// type derived by restriction must have a content model that accepts a
|
|
/// subset of its base's. Covers element-vs-element name matching,
|
|
/// element-vs-wildcard admission, wildcard narrowing, occurrence-range
|
|
/// subsumption, and the group recursions (sequence/all order-preserving with
|
|
/// emptiable skips, choice mapping, sequence-into-choice, sequence-into-all).
|
|
/// The spec's effective-total-range arithmetic for MapAndSum is approximated
|
|
/// by per-particle checks, a documented simplification.
|
|
enum ParticleRestriction {
|
|
/// The reason a restriction's content is not a valid subset of its
|
|
/// base's, or nil when it is. `types` resolves an element's
|
|
/// ``ElementType/typeReference`` so element-vs-element pairs can check that
|
|
/// the restricting type derives from the base's (NameAndTypeOK).
|
|
static func violation(restricted: ContentType, base: ContentType, types: [String: ElementType], derivation: [String: TypeDerivation]) -> String? {
|
|
switch (restricted, base) {
|
|
case (.empty, .empty), (.simpleContent, _), (_, .simpleContent):
|
|
nil
|
|
case let (.empty, .elementOnly(baseParticle)), let (.empty, .mixed(baseParticle)):
|
|
emptiable(baseParticle) ? nil : "the base content is required, so the restriction cannot be EMPTY"
|
|
case (.mixed, .elementOnly):
|
|
"a restriction cannot add mixed content to an element-only base"
|
|
case let (.elementOnly(restrictedParticle), .empty):
|
|
// A content-free particle (an empty group, or one whose members all
|
|
// have maxOccurs=0) accepts only the empty sequence, so it is a valid
|
|
// restriction of EMPTY; only a particle that can contribute a child is
|
|
// adding content.
|
|
contentFree(restrictedParticle) ? nil : "the base content is EMPTY, so the restriction cannot add content"
|
|
case (.mixed, .empty):
|
|
"the base content is EMPTY, so the restriction cannot add content"
|
|
case let (.elementOnly(restrictedParticle), .elementOnly(baseParticle)),
|
|
let (.elementOnly(restrictedParticle), .mixed(baseParticle)),
|
|
let (.mixed(restrictedParticle), .mixed(baseParticle)):
|
|
// The W3C restriction algorithm is defined over the normalized content
|
|
// model (pointless particles eliminated): a `maxOccurs=0` member
|
|
// removed, a `{1,1}` single-member group unwrapped, a nested
|
|
// same-compositor `{1,1}` group spliced. Without this, e.g. a base
|
|
// `sequence(any{2,3})` is not seen as the wildcard it equals.
|
|
valid(restrictedParticle.normalized(), baseParticle.normalized(), types, derivation)
|
|
? nil
|
|
: "the restricted content model is not a subset of the base's"
|
|
}
|
|
}
|
|
|
|
/// Whether `restricted` accepts a subset of `base` (the pairwise check).
|
|
///
|
|
/// Occurrence subsumption (`rangeSubsumed`) is applied only to the
|
|
/// same-kind pairings (element/element, wildcard/wildcard, group/group) and
|
|
/// element/wildcard, where the two particles' own occurrence ranges are
|
|
/// directly comparable. For the cross-kind pairings (element/group and
|
|
/// group/element via RecurseAsIfGroup; group/wildcard via
|
|
/// NSRecurseCheckCardinality) the occurrence is matched *inside* the case,
|
|
/// against a member or as an effective total range, never the derived
|
|
/// particle's own occurrence against the base group/wildcard's. Applying the
|
|
/// outer check there over-rejects: e.g. `sequence(e1{2,3})` restricting
|
|
/// `sequence(e1{1,3},...)` once the pointless single-member group is
|
|
/// normalized to the bare `e1{2,3}` (`3 > 1` against the base sequence).
|
|
static func valid(_ restricted: Particle, _ base: Particle, _ types: [String: ElementType], _ derivation: [String: TypeDerivation]) -> Bool {
|
|
switch (restricted.term, base.term) {
|
|
case let (.element(restrictedName, _, restrictedTypeName), .element(baseName, _, baseTypeName)):
|
|
return rangeSubsumed(restricted, base)
|
|
&& sameName(restrictedName, baseName)
|
|
&& elementTypeRestrictionOK(restrictedTypeName, baseTypeName, types, derivation)
|
|
case let (.element(name, _, _), .wildcard(wildcard)):
|
|
return rangeSubsumed(restricted, base) && wildcard.admits(name)
|
|
case let (.wildcard(restrictedWildcard), .wildcard(baseWildcard)):
|
|
return rangeSubsumed(restricted, base) && narrows(restrictedWildcard, baseWildcard)
|
|
case let (.element, .group(baseGroup)):
|
|
// RecurseAsIfGroup: the lone element, as a one-particle sequence,
|
|
// against the base group. The element's occurrence is matched against
|
|
// a base member inside `groupValid`, not against the base group's own
|
|
// occurrence, so no outer `rangeSubsumed` here.
|
|
return groupValid(Group(compositor: .sequence, particles: [restricted]), baseGroup, types, derivation)
|
|
case let (.group, .element(baseName, _, baseTypeName)):
|
|
// The mirror of RecurseAsIfGroup, by the same cardinality argument as
|
|
// group/wildcard: a base element accepts only its own name, its own
|
|
// occurrence count of times. The derived group is a valid restriction
|
|
// iff every leaf it can contain is that element (same name, and a type
|
|
// that derives from the base's) AND the group's effective total
|
|
// occurrence range is within the base element's. Comparing the derived
|
|
// group's *own* occurrence to the element's would be wrong (a derived
|
|
// `sequence(a,a)` emits two a's at occurrence {1,1}); the effective
|
|
// range is the count that must be bounded.
|
|
return leavesRestrict(restricted, toElement: baseName, baseTypeName, types, derivation)
|
|
&& rangeWithinWildcard(
|
|
derivedMin: restricted.effectiveOccurrenceMin(),
|
|
derivedMax: restricted.effectiveOccurrenceMax(),
|
|
wildcardMin: base.minOccurs,
|
|
wildcardMax: base.maxOccurs,
|
|
)
|
|
case let (.group(restrictedGroup), .group(baseGroup)):
|
|
// If every member of the derived group is a pointless (maxOccurs=0)
|
|
// particle, the group accepts only the empty sequence; that is a valid
|
|
// restriction iff the base particle is itself emptiable (by structure
|
|
// or its own occurrence). `emptiable(base)` accounts for both, which a
|
|
// structural check inside `groupValid` could not.
|
|
if restrictedGroup.particles.allSatisfy({ $0.maxOccurs == 0 }) {
|
|
return emptiable(base)
|
|
}
|
|
return rangeSubsumed(restricted, base) && groupValid(restrictedGroup, baseGroup, types, derivation)
|
|
case let (.group, .wildcard(wildcard)):
|
|
// NSRecurseCheckCardinality: every leaf the group can contain is
|
|
// admitted by the wildcard (an element by name, a nested wildcard by
|
|
// narrowing), AND the group's effective total occurrence range is
|
|
// within the wildcard's occurrence range.
|
|
return leavesAdmitted(restricted, by: wildcard)
|
|
&& rangeWithinWildcard(
|
|
derivedMin: restricted.effectiveOccurrenceMin(),
|
|
derivedMax: restricted.effectiveOccurrenceMax(),
|
|
wildcardMin: base.minOccurs,
|
|
wildcardMax: base.maxOccurs,
|
|
)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private static func groupValid(_ restricted: Group, _ base: Group, _ types: [String: ElementType], _ derivation: [String: TypeDerivation]) -> Bool {
|
|
// A particle that can never occur (maxOccurs=0) contributes nothing to the
|
|
// language, so it is removed before mapping (a "pointless particle"). Left
|
|
// in, it would wrongly consume a base particle and starve the particles
|
|
// after it (e.g. `(e1 maxOccurs=0, e2)` restricting `(any)`).
|
|
let restrictedParticles = restricted.particles.filter { $0.maxOccurs != 0 }
|
|
let baseParticles = base.particles.filter { $0.maxOccurs != 0 }
|
|
switch (restricted.compositor, base.compositor) {
|
|
case (.sequence, .sequence), (.all, .all):
|
|
return recurse(restrictedParticles, baseParticles, skippedMustBeEmptiable: true, types, derivation)
|
|
case (.choice, .choice):
|
|
return recurse(restrictedParticles, baseParticles, skippedMustBeEmptiable: false, types, derivation)
|
|
case (.sequence, .choice):
|
|
// MapAndSum (simplified): each restricted particle fits some branch.
|
|
return restrictedParticles.allSatisfy { particle in
|
|
baseParticles.contains { valid(particle, $0, types, derivation) }
|
|
}
|
|
case (.sequence, .all):
|
|
// RecurseUnordered: map onto distinct base particles in any order.
|
|
return recurseUnordered(restrictedParticles, baseParticles, types, derivation)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Whether a particle can never contribute a child element or wildcard: it
|
|
/// has `maxOccurs=0`, or is a group all of whose members are content-free
|
|
/// (an empty group included). Its only legal content is the empty sequence.
|
|
private static func contentFree(_ particle: Particle) -> Bool {
|
|
if particle.maxOccurs == 0 { return true }
|
|
switch particle.term {
|
|
case .element, .wildcard:
|
|
return false
|
|
case let .group(group):
|
|
return group.particles.allSatisfy(contentFree)
|
|
}
|
|
}
|
|
|
|
/// Order-preserving mapping of the restricted particles onto the base's.
|
|
/// A skipped base particle must be emptiable for sequence/all (Recurse);
|
|
/// for choice (RecurseLax) skipping is free.
|
|
private static func recurse(
|
|
_ restricted: [Particle],
|
|
_ base: [Particle],
|
|
skippedMustBeEmptiable: Bool,
|
|
_ types: [String: ElementType],
|
|
_ derivation: [String: TypeDerivation],
|
|
) -> Bool {
|
|
var baseIndex = 0
|
|
for particle in restricted {
|
|
var matched = false
|
|
while baseIndex < base.count {
|
|
let candidate = base[baseIndex]
|
|
baseIndex += 1
|
|
if valid(particle, candidate, types, derivation) {
|
|
matched = true
|
|
break
|
|
}
|
|
if skippedMustBeEmptiable, !emptiable(candidate) { return false }
|
|
}
|
|
if !matched { return false }
|
|
}
|
|
if skippedMustBeEmptiable {
|
|
while baseIndex < base.count {
|
|
if !emptiable(base[baseIndex]) { return false }
|
|
baseIndex += 1
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// Any-order mapping onto distinct base particles; unmapped base
|
|
/// particles must be emptiable.
|
|
private static func recurseUnordered(_ restricted: [Particle], _ base: [Particle], _ types: [String: ElementType], _ derivation: [String: TypeDerivation]) -> Bool {
|
|
var used = [Bool](repeating: false, count: base.count)
|
|
for particle in restricted {
|
|
guard let index = base.indices.first(where: { !used[$0] && valid(particle, base[$0], types, derivation) }) else {
|
|
return false
|
|
}
|
|
used[index] = true
|
|
}
|
|
return base.indices.allSatisfy { used[$0] || emptiable(base[$0]) }
|
|
}
|
|
|
|
/// NameAndTypeOK type clause: the restricting element's type must be, or
|
|
/// derive by restriction from, the base element's type. Element types carry
|
|
/// their derivation identity as a `typeName` (a built-in, a user type, or
|
|
/// `anyType` for an absent type; nil for an inline anonymous type). When both
|
|
/// names are known the answer is decided by name; an inline anonymous type on
|
|
/// either side is permitted (its derivation identity is not recorded), which
|
|
/// can only under-reject, so no valid restriction is rejected.
|
|
private static func elementTypeRestrictionOK(
|
|
_ restrictedTypeName: String?,
|
|
_ baseTypeName: String?,
|
|
_ types: [String: ElementType],
|
|
_ derivation: [String: TypeDerivation],
|
|
) -> Bool {
|
|
guard let restrictedTypeName, let baseTypeName else { return true }
|
|
// A type validly derives from a union by being one of its members, which
|
|
// is not a restriction chain over type names, so when the base resolves to
|
|
// a union variety the name check stands down (permit) rather than misread
|
|
// the membership as unrelated. (A list base stays checked: a valid
|
|
// list-typed restriction is the same named list or a restriction of it,
|
|
// which the derivation chain does capture.)
|
|
if baseIsUnion(baseTypeName, types) { return true }
|
|
return typeDerivesOrEqual(restrictedTypeName, baseTypeName, derivation, types)
|
|
}
|
|
|
|
/// Whether the named type resolves (through the type table) to a `union`
|
|
/// simple type.
|
|
private static func baseIsUnion(_ name: String, _ types: [String: ElementType]) -> Bool {
|
|
var current: ElementType? = types[name]
|
|
var steps = 0
|
|
while let resolved = current, steps <= types.count {
|
|
switch resolved {
|
|
case let .simple(simple):
|
|
if case .union = simple.variety { return true }
|
|
return false
|
|
case .complex:
|
|
return false
|
|
case let .typeReference(key):
|
|
current = types[key]
|
|
steps += 1
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private static let urTypeNames: Set<String> = ["anyType", "anySimpleType", "anyAtomicType"]
|
|
|
|
private static func isUrTypeName(_ name: String, _ types: [String: ElementType]) -> Bool {
|
|
urTypeNames.contains(name) && types[name] == nil
|
|
}
|
|
|
|
/// Whether the type named `derived` is, or is validly derived by restriction
|
|
/// from, the type named `base`: any type derives from an ur-type base; a name
|
|
/// equal to the base matches; the user-declared restriction chain is walked
|
|
/// (`derivation`), and where it bottoms out in a built-in the lattice
|
|
/// continues it. Names are compared as the compiler resolves them (by local
|
|
/// name), so an unrelated pair (`xs:string` restricting a user type, two
|
|
/// independent list types) is correctly not derivable.
|
|
private static func typeDerivesOrEqual(_ derived: String, _ base: String, _ derivation: [String: TypeDerivation], _ types: [String: ElementType]) -> Bool {
|
|
if isUrTypeName(base, types) { return true }
|
|
var current = derived
|
|
var visited: Set<String> = []
|
|
while visited.insert(current).inserted {
|
|
if current == base { return true }
|
|
guard let step = derivation[current] else { break }
|
|
current = step.base
|
|
}
|
|
if let derivedBuiltin = BuiltinType(rawValue: current), let baseBuiltin = BuiltinType(rawValue: base) {
|
|
return derivedBuiltin.derives(from: baseBuiltin)
|
|
}
|
|
return current == base
|
|
}
|
|
|
|
/// Occurrence-range subsumption: the restriction may not occur less often
|
|
/// than the base requires, nor more often than the base allows.
|
|
private static func rangeSubsumed(_ restricted: Particle, _ base: Particle) -> Bool {
|
|
guard restricted.minOccurs >= base.minOccurs else { return false }
|
|
guard let baseMax = base.maxOccurs else { return true }
|
|
guard let restrictedMax = restricted.maxOccurs else { return false }
|
|
return restrictedMax <= baseMax
|
|
}
|
|
|
|
/// Whether a particle can match no children at all.
|
|
static func emptiable(_ particle: Particle) -> Bool {
|
|
if particle.minOccurs == 0 { return true }
|
|
switch particle.term {
|
|
case .element, .wildcard:
|
|
return false
|
|
case let .group(group):
|
|
switch group.compositor {
|
|
case .sequence, .all:
|
|
return group.particles.allSatisfy(emptiable)
|
|
case .choice:
|
|
return group.particles.contains(where: emptiable)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether the restricted wildcard admits no namespace the base refuses.
|
|
private static func narrows(_ restricted: Wildcard, _ base: Wildcard) -> Bool {
|
|
switch (restricted.namespace, base.namespace) {
|
|
case (_, .any):
|
|
true
|
|
case (.other, .other):
|
|
restricted.targetNamespace == base.targetNamespace
|
|
case let (.enumerated(narrow), .enumerated(wide)):
|
|
narrow.isSubset(of: wide)
|
|
case let (.enumerated(narrow), .other):
|
|
!narrow.contains("") && !narrow.contains(base.targetNamespace ?? "")
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
private static func sameName(_ lhs: PureXML.Model.QualifiedName, _ rhs: PureXML.Model.QualifiedName) -> Bool {
|
|
lhs.localName == rhs.localName && (lhs.namespaceURI ?? "") == (rhs.namespaceURI ?? "")
|
|
}
|
|
|
|
/// Whether every leaf a particle can contain is admitted by `wildcard`: an
|
|
/// element by name, a nested wildcard by narrowing.
|
|
private static func leavesAdmitted(_ particle: Particle, by wildcard: Wildcard) -> Bool {
|
|
switch particle.term {
|
|
case let .element(name, _, _): wildcard.admits(name)
|
|
case let .wildcard(inner): narrows(inner, wildcard)
|
|
case let .group(group): group.particles.allSatisfy { leavesAdmitted($0, by: wildcard) }
|
|
}
|
|
}
|
|
|
|
/// Whether every leaf a particle can contain is the named base element (same
|
|
/// name, and a type that derives from the base element's). A wildcard leaf is
|
|
/// rejected: it can match names other than the base element's, so it is not a
|
|
/// subset. Used when the base content model is a single element.
|
|
private static func leavesRestrict(
|
|
_ particle: Particle,
|
|
toElement baseName: PureXML.Model.QualifiedName,
|
|
_ baseTypeName: String?,
|
|
_ types: [String: ElementType],
|
|
_ derivation: [String: TypeDerivation],
|
|
) -> Bool {
|
|
switch particle.term {
|
|
case let .element(name, _, typeName):
|
|
sameName(name, baseName) && elementTypeRestrictionOK(typeName, baseTypeName, types, derivation)
|
|
case .wildcard:
|
|
false
|
|
case let .group(group):
|
|
group.particles.allSatisfy { leavesRestrict($0, toElement: baseName, baseTypeName, types, derivation) }
|
|
}
|
|
}
|
|
|
|
/// Whether a derived effective total range fits within a base wildcard's
|
|
/// occurrence range: at least the wildcard's minimum, at most its maximum.
|
|
private static func rangeWithinWildcard(derivedMin: Int, derivedMax: Int?, wildcardMin: Int, wildcardMax: Int?) -> Bool {
|
|
guard derivedMin >= wildcardMin else { return false }
|
|
guard let wildcardMax else { return true }
|
|
guard let derivedMax else { return false }
|
|
return derivedMax <= wildcardMax
|
|
}
|
|
}
|
|
}
|