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).
149 lines
8.1 KiB
Swift
149 lines
8.1 KiB
Swift
extension PureXML.Schema.ComplexValidator {
|
|
/// The first `xsi:type` override error for `child`, if any: a substitution
|
|
/// blocked by `block`, a substitute not validly derived from the declared type
|
|
/// (cvc-elt.4.3.2.1), or a missing `xsi:type` on an abstract declared type.
|
|
///
|
|
/// The derivation backbone (`typeDerivation`), `block` tables, and abstract-type
|
|
/// set are keyed by namespaced identity (`{ns}local`), so two imported types
|
|
/// sharing a local name in different namespaces do not collide. The declared
|
|
/// type's reference (`type:{ns}local`) and the instance `xsi:type` (resolved
|
|
/// through the element's prefix bindings) are both reduced to that key.
|
|
func xsiTypeOverrideError(
|
|
declared: PureXML.Schema.ElementType,
|
|
child: PureXML.Model.Element,
|
|
at path: [PureXML.Validation.PathKey],
|
|
namespaceBindings: [String: String],
|
|
) -> PureXML.Validation.ValidationError? {
|
|
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked }
|
|
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived }
|
|
if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
|
|
return nil
|
|
}
|
|
|
|
/// The error when an element's declared type is an abstract complex type and
|
|
/// the element supplies no `xsi:type` to name a concrete derived type. An
|
|
/// abstract type cannot itself be the type of an instance element.
|
|
func abstractTypeError(
|
|
named name: String,
|
|
child: PureXML.Model.Element,
|
|
at path: [PureXML.Validation.PathKey],
|
|
) -> PureXML.Validation.ValidationError? {
|
|
guard abstractTypes.contains(resolvedDeclaredTypeName(name)),
|
|
Self.xsiTypeName(child) == nil
|
|
else { return nil }
|
|
let bare = PureXML.Schema.XSDParser.bareTypeLocalName(name)
|
|
return PureXML.Validation.ValidationError(
|
|
reason: "element '\(child.name.localName)' has abstract type '\(bare)' and requires an xsi:type naming a concrete derived type",
|
|
at: path,
|
|
)
|
|
}
|
|
|
|
/// The error when an `xsi:type` substitution is forbidden by `block` on the
|
|
/// declared type: the substituted type reaches the declared type by a method
|
|
/// the declared type lists in `block`. Returns nil when there is no named
|
|
/// declared type, no `block`, or the substitution is permitted.
|
|
func blockedSubstitutionError(
|
|
declared: PureXML.Schema.ElementType,
|
|
child: PureXML.Model.Element,
|
|
at path: [PureXML.Validation.PathKey],
|
|
namespaceBindings: [String: String],
|
|
) -> PureXML.Validation.ValidationError? {
|
|
guard case let .typeReference(reference) = declared,
|
|
let substituteLabel = Self.xsiTypeName(child),
|
|
let substituteRef = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
|
|
Self.resolveNamedType(substituteRef, in: types) != nil
|
|
else {
|
|
return nil
|
|
}
|
|
let substituteKey = Self.derivationKey(fromReference: substituteRef)
|
|
let declaredName = resolvedDeclaredTypeName(reference)
|
|
guard let methods = PureXML.Schema.XSDParser.derivationMethods(from: substituteKey, to: declaredName, typeDerivation) else {
|
|
return nil
|
|
}
|
|
// The substitution is blocked when the derivation method is disallowed by
|
|
// either the declared type's `block` or the element declaration's own
|
|
// `block`, both keyed by namespaced identity.
|
|
let blocked = (typeBlock[declaredName] ?? []).union(elementBlock[Self.key(child.name)] ?? [])
|
|
guard !methods.isDisjoint(with: blocked) else {
|
|
return nil
|
|
}
|
|
return PureXML.Validation.ValidationError(
|
|
reason: "xsi:type '\(substituteLabel)' is blocked: substitution by this derivation is disallowed",
|
|
at: path,
|
|
)
|
|
}
|
|
|
|
/// cvc-elt.4.3.2.1: an `xsi:type` must resolve to a type DERIVED from the
|
|
/// element's declared type. This flags ONLY the case where both the declared
|
|
/// type and the substitute resolve to COMPLEX types recorded in the derivation
|
|
/// backbone and there is no derivation path between them (a different branch of
|
|
/// the hierarchy). It deliberately does NOT touch a simple/list/union substitute,
|
|
/// a substitute or declared type absent from the backbone, or the ur-types: there
|
|
/// the relationship cannot be confirmed from the complex-type backbone alone, so
|
|
/// the rule stays silent (under-reject) rather than risk a false positive.
|
|
func notDerivedSubstitutionError(
|
|
declared: PureXML.Schema.ElementType,
|
|
child: PureXML.Model.Element,
|
|
at path: [PureXML.Validation.PathKey],
|
|
namespaceBindings: [String: String],
|
|
) -> PureXML.Validation.ValidationError? {
|
|
guard case let .typeReference(reference) = declared,
|
|
let substituteLabel = Self.xsiTypeName(child),
|
|
let substituteRef = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
|
|
case .complex? = Self.resolveNamedType(substituteRef, in: types)
|
|
else { return nil }
|
|
let substituteKey = Self.derivationKey(fromReference: substituteRef)
|
|
let declaredName = resolvedDeclaredTypeName(reference)
|
|
guard substituteKey != declaredName,
|
|
typeDerivation[substituteKey] != nil,
|
|
typeDerivation[declaredName] != nil || isComplexBackboneRoot(declaredName),
|
|
PureXML.Schema.XSDParser.derivationMethods(from: substituteKey, to: declaredName, typeDerivation) == nil
|
|
else { return nil }
|
|
return PureXML.Validation.ValidationError(
|
|
reason: "xsi:type '\(substituteLabel)' is not validly derived from the declared type of '\(child.name.localName)'",
|
|
at: path,
|
|
)
|
|
}
|
|
|
|
/// A complex type that heads a derivation backbone (some recorded type derives
|
|
/// from it) even though it records no base of its own, so it is a legitimate
|
|
/// declared-type target for the not-derived check.
|
|
private func isComplexBackboneRoot(_ name: String) -> Bool {
|
|
let local = PureXML.Schema.XSDParser.unpackElementName(name).0
|
|
guard local != "anyType", local != "anySimpleType" else { return false }
|
|
return typeDerivation.values.contains { $0.base == name }
|
|
}
|
|
|
|
/// Follows an element-ref / type-reference chain (`element:foo` -> concrete type)
|
|
/// to the declared type's namespaced derivation key (`{ns}local`), which the
|
|
/// derivation backbone is keyed by.
|
|
private func resolvedDeclaredTypeName(_ reference: String) -> String {
|
|
var key = reference
|
|
var name = Self.derivationKey(fromReference: reference)
|
|
var steps = 0
|
|
while steps <= types.count {
|
|
guard let resolved = types[key] ?? Self.resolveNamedType(key, in: types),
|
|
case let .typeReference(next) = resolved
|
|
else { break }
|
|
key = next
|
|
name = Self.derivationKey(fromReference: next)
|
|
steps += 1
|
|
}
|
|
return name
|
|
}
|
|
|
|
/// The namespaced derivation key (`{ns}local`) a type-table reference reduces
|
|
/// to. A `type:{ns}local` key has its `type:` prefix stripped to leave the
|
|
/// `{ns}local` identity. A bare reference (an unprefixed `xsi:type` resolved in
|
|
/// no namespace, or a built-in name) is normalized to `{}local`, matching the
|
|
/// no-namespace key the backbone records for an unqualified component. An
|
|
/// `element:`/other-keyed reference is left as-is, where it will not match the
|
|
/// namespaced backbone and the check stays silent rather than risk a false
|
|
/// positive.
|
|
static func derivationKey(fromReference reference: String) -> String {
|
|
if reference.hasPrefix("type:") { return String(reference.dropFirst("type:".count)) }
|
|
if reference.hasPrefix("{") || reference.contains(":") { return reference }
|
|
return key(PureXML.Model.QualifiedName(localName: reference, namespaceURI: nil))
|
|
}
|
|
}
|