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).
99 lines
5.0 KiB
Swift
99 lines
5.0 KiB
Swift
extension PureXML.Schema.XSDParser {
|
|
/// Value-space validity of the `final` and `block` derivation-control
|
|
/// attributes (XSD 1.0 Structures). Each is `#all` or a whitespace-separated
|
|
/// list of method tokens, but the admitted tokens depend on the component:
|
|
///
|
|
/// - element `final`: `extension`, `restriction`
|
|
/// - element `block`: `extension`, `restriction`, `substitution`
|
|
/// - complexType `final`/`block`: `extension`, `restriction`
|
|
/// - simpleType `final`: `list`, `union`, `restriction`
|
|
///
|
|
/// `methodSet` parses these leniently (ignoring unknown tokens and admitting
|
|
/// `substitution` everywhere), so `final="substitution"` on an element, a
|
|
/// misspelled or miscased token (`foo`, `#All`, `Extension`), or `#all` mixed
|
|
/// with other tokens was accepted (the elemF / st_final families). The token
|
|
/// list is now checked against the component's exact set.
|
|
static func derivationControlErrors(_ node: XSDTree, local: String) -> [String] {
|
|
var errors: [String] = []
|
|
for attribute in ["final", "block"] {
|
|
guard let allowed = derivationControlTokens["\(local)|\(attribute)"],
|
|
let raw = node.attributes.first(where: { $0.name.prefix == nil && $0.name.localName == attribute })?.value
|
|
else { continue }
|
|
if let reason = derivationListError(raw, allowed: allowed) {
|
|
errors.append("the '\(attribute)' value '\(raw)' on '\(local)' is not valid: \(reason)")
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/// The exact method tokens each component admits in `final`/`block`.
|
|
private static let derivationControlTokens: [String: Set<String>] = [
|
|
"element|final": ["extension", "restriction"],
|
|
"element|block": ["extension", "restriction", "substitution"],
|
|
"complexType|final": ["extension", "restriction"],
|
|
"complexType|block": ["extension", "restriction"],
|
|
"simpleType|final": ["list", "union", "restriction"],
|
|
]
|
|
|
|
/// The reason a `final`/`block` value is invalid, or nil. `#all` stands alone;
|
|
/// otherwise every whitespace-separated token must be in `allowed` (exact case),
|
|
/// and `#all` may not be mixed with other tokens. An empty value is valid (no
|
|
/// control).
|
|
private static func derivationListError(_ raw: String, allowed: Set<String>) -> String? {
|
|
let tokens = raw.split(whereSeparator: \.isWhitespace).map(String.init)
|
|
if tokens == ["#all"] { return nil }
|
|
for token in tokens {
|
|
if token == "#all" { return "'#all' may not be combined with other tokens" }
|
|
if !allowed.contains(token) { return "'\(token)' is not one of \(allowed.sorted().joined(separator: ", "))" }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// The `{final}` derivation methods of a complex type or element: its own
|
|
/// `final` when present, otherwise the enclosing schema's `finalDefault`, each
|
|
/// intersected with the extension/restriction methods these components admit. An
|
|
/// explicit `final=""` overrides the default back to no restriction.
|
|
static func finalMethods(of node: XSDTree) -> Set<PureXML.Schema.DerivationMethod> {
|
|
if let own = PureXML.Schema.XSDNode.attribute(node, "final") {
|
|
return methodSet(own).intersection([.extension, .restriction])
|
|
}
|
|
if let finalDefault = schemaFinalDefault(of: node) {
|
|
return methodSet(finalDefault).intersection([.extension, .restriction])
|
|
}
|
|
return []
|
|
}
|
|
|
|
/// The `{prohibited substitutions}` (complex type) or `{disallowed
|
|
/// substitutions}` (element) of a component: its own `block` when present,
|
|
/// otherwise the enclosing schema's `blockDefault`, each intersected with the
|
|
/// methods that component admits (`admitting`). An explicit `block=""` overrides
|
|
/// the default back to no restriction (XSD 1.0 Structures §3.3.2/§3.4.2).
|
|
static func blockMethods(of node: XSDTree, admitting: Set<PureXML.Schema.DerivationMethod>) -> Set<PureXML.Schema.DerivationMethod> {
|
|
if let own = PureXML.Schema.XSDNode.attribute(node, "block") {
|
|
return methodSet(own).intersection(admitting)
|
|
}
|
|
if let blockDefault = schemaDefault(of: node, named: "blockDefault") {
|
|
return methodSet(blockDefault).intersection(admitting)
|
|
}
|
|
return []
|
|
}
|
|
|
|
/// The `finalDefault` of the `schema` enclosing `node`, or nil.
|
|
static func schemaFinalDefault(of node: XSDTree) -> String? {
|
|
schemaDefault(of: node, named: "finalDefault")
|
|
}
|
|
|
|
/// The named attribute (`finalDefault`/`blockDefault`) of the `schema` element
|
|
/// enclosing `node`, or nil.
|
|
private static func schemaDefault(of node: XSDTree, named attribute: String) -> String? {
|
|
var current: XSDTree? = node
|
|
while let element = current {
|
|
if element.name?.namespaceURI == xsdNamespace, PureXML.Schema.XSDNode.localName(element) == "schema" {
|
|
return PureXML.Schema.XSDNode.attribute(element, attribute)
|
|
}
|
|
current = element.parent
|
|
}
|
|
return nil
|
|
}
|
|
}
|