The instance-validity derivation and substitution tables were keyed by bare
local name, so two same-local-name types or elements in different namespaces
collided. In multi-document validation this let one schema's block/derivation
facts shadow another's, and it blocked honouring blockDefault (whose re-land
widened that collision into an over-rejection).
Key the instance subsystem by namespaced identity {ns}local instead. The shared
DerivationTables keeps its bare-keyed fields for the schema-validity machinery
(Particle-Valid-Restriction and the consistency rules, which resolve QNames to
bare names and walk the same backbone) and gains parallel ns* fields built in
the same pass, resolving each container's components through its target
namespace and prefix bindings. Only the instance consumers
(blockedSubstitutionError, notDerivedSubstitutionError, abstractTypeError, the
substitution-group closure, and referenceParticle) read the ns* tables, via a
derivationKey(fromReference:) that reduces a type:{ns}local reference, a bare
name, or an element ref to a {ns}local backbone key. A key miss stays permissive
(no constraint, substitution allowed), so the re-keying cannot over-reject.
With the namespaced backbone in place, blockDefault is re-landed FP-free: a
schema's blockDefault supplies the {disallowed substitutions} of every element
and complex type without an explicit block (own block overrides; block="" clears;
intersected with the methods each component admits), so blockDefault="extension"
bars an extension substitution and "#all" bars all, without one namespace's
defaults shadowing another's.
The pure re-keying preserves single-document behaviour exactly (the corpus is
overwhelmingly single-namespace, where {ns}local and bare coincide): all four
XSTS buckets held at vsr=0 / isa=69 / vir=0 / ivi=89 after re-keying alone.
blockDefault then drove invalid-instances-accepted 89 to 81, no false positive
(valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket
moved. An adversarial review exercised same- and cross-namespace substitutions,
no-targetNamespace keys, simple-type restriction, transitive substitution groups,
and blockDefault override/over-block cases, and the prior multi-document
cross-namespace blockDefault over-rejection is now accepted while a same-namespace
blocked substitution is still rejected (precise, not merely permissive).
144 lines
6.6 KiB
Swift
144 lines
6.6 KiB
Swift
private typealias XSDNode = PureXML.Schema.XSDNode
|
|
private typealias XSDContext = PureXML.Schema.XSDContext
|
|
private typealias Particle = PureXML.Schema.Particle
|
|
private typealias Compositor = PureXML.Schema.Compositor
|
|
private typealias Group = PureXML.Schema.Group
|
|
private typealias ElementType = PureXML.Schema.ElementType
|
|
|
|
extension PureXML.Schema.XSDParser {
|
|
// MARK: Model groups
|
|
|
|
static func modelGroup(in node: PureXML.Model.TreeNode, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Particle? {
|
|
for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] {
|
|
if let group = XSDNode.firstChild(node, named: name) {
|
|
return groupParticle(group, compositor, context)
|
|
}
|
|
}
|
|
if let groupRef = XSDNode.firstChild(node, named: "group") {
|
|
return particle(groupRef, context)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func groupParticle(
|
|
_ node: PureXML.Model.TreeNode,
|
|
_ compositor: Compositor,
|
|
_ context: XSDContext,
|
|
) -> Particle {
|
|
var particles: [Particle] = []
|
|
for child in XSDNode.elementChildren(node) {
|
|
if let member = particle(child, context) { particles.append(member) }
|
|
}
|
|
let (minimum, maximum) = XSDNode.occurrence(node)
|
|
if compositor == .all, minimum == 0 {
|
|
particles = particles.map { Particle(minOccurs: 0, maxOccurs: $0.maxOccurs, term: $0.term) }
|
|
}
|
|
return Particle(
|
|
minOccurs: minimum,
|
|
maxOccurs: maximum,
|
|
term: .group(Group(compositor: compositor, particles: particles)),
|
|
)
|
|
}
|
|
|
|
private static func particle(_ node: PureXML.Model.TreeNode, _ context: XSDContext) -> Particle? {
|
|
let (minimum, maximum) = XSDNode.occurrence(node)
|
|
switch XSDNode.localName(node) {
|
|
case "element":
|
|
return elementParticle(node, minimum, maximum, context)
|
|
case "sequence":
|
|
return groupParticle(node, .sequence, context)
|
|
case "choice":
|
|
return groupParticle(node, .choice, context)
|
|
case "all":
|
|
return groupParticle(node, .all, context)
|
|
case "group":
|
|
return groupReferenceParticle(node, minimum, maximum, context)
|
|
case "any":
|
|
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(wildcard(node, context)))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func elementParticle(_ node: PureXML.Model.TreeNode, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle {
|
|
if let ref = XSDNode.attribute(node, "ref") {
|
|
return referenceParticle(ref, minimum, maximum, context, node: node)
|
|
}
|
|
let name = XSDNode.attribute(node, "name") ?? ""
|
|
return Particle(
|
|
minOccurs: minimum,
|
|
maxOccurs: maximum,
|
|
term: .element(
|
|
name: localElementName(name, XSDNode.attribute(node, "form"), context),
|
|
type: elementType(node, context),
|
|
typeName: elementTypeName(node),
|
|
valueConstraint: valueConstraint(of: node),
|
|
block: methodSet(XSDNode.attribute(node, "block") ?? context.blockDefault),
|
|
nillable: ["true", "1"].contains(XSDNode.attribute(node, "nillable")),
|
|
),
|
|
)
|
|
}
|
|
|
|
private static func groupReferenceParticle(_ node: PureXML.Model.TreeNode, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle? {
|
|
guard let ref = XSDNode.attribute(node, "ref") else { return nil }
|
|
let name = XSDNode.stripPrefix(ref)
|
|
if context.visitingGroups.contains(name) {
|
|
guard context.redefinedGroups.contains(name),
|
|
let base = context.baseGroups[name]
|
|
else {
|
|
return nil
|
|
}
|
|
let scoped = context.scoped(for: XSDNode.schemaOwner(base))
|
|
guard let inner = modelGroup(in: base, scoped.visiting(name)) else {
|
|
return nil
|
|
}
|
|
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
|
|
}
|
|
guard let definition = context.groups[name] else {
|
|
return nil
|
|
}
|
|
let scoped = context.scoped(for: XSDNode.schemaOwner(definition))
|
|
guard let inner = modelGroup(in: definition, scoped.visiting(name)) else {
|
|
return nil
|
|
}
|
|
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
|
|
}
|
|
|
|
/// The particle for an `<xs:element ref="...">`. An abstract head may not appear
|
|
/// itself, only its substitution-group members; a concrete head appears alongside
|
|
/// them, the reference expanding to a choice over the head and every member.
|
|
static func referenceParticle(
|
|
_ ref: String,
|
|
_ minimum: Int,
|
|
_ maximum: Int?,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
node: XSDTree? = nil,
|
|
) -> PureXML.Schema.Particle {
|
|
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
|
let bindings = node.map { PureXML.Schema.XSDParser.namespaceBindingsInScope(of: $0, defaultBindings: context.namespaceBindings) }
|
|
?? context.namespaceBindings
|
|
let refNamespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings)
|
|
let headKey = PureXML.Schema.XSDParser.derivationKey(name, in: refNamespace)
|
|
let head: [(String, String?)] = context.abstractElements.contains(name) ? [] : [(name, refNamespace)]
|
|
// Members are namespaced keys (`{ns}local`): unpack each to its local name
|
|
// and namespace so the choice over the head and its members keeps every
|
|
// member's own namespace, even across imported namespaces.
|
|
let alternatives = head + (context.substitutions[headKey] ?? []).map { PureXML.Schema.XSDParser.unpackElementName($0) }
|
|
if alternatives.count == 1 {
|
|
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: elementReferenceTerm(alternatives[0].0, alternatives[0].1))
|
|
}
|
|
let members = alternatives.map { PureXML.Schema.Particle(term: elementReferenceTerm($0.0, $0.1)) }
|
|
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: .group(.init(compositor: .choice, particles: members)))
|
|
}
|
|
|
|
/// The term for a reference to a global element, in the resolved `namespace`.
|
|
static func elementReferenceTerm(_ name: String, _ namespace: String?) -> PureXML.Schema.Term {
|
|
.element(
|
|
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: namespace),
|
|
type: .typeReference(elementKey(name)),
|
|
typeName: nil,
|
|
valueConstraint: nil,
|
|
)
|
|
}
|
|
}
|