PureXML/Sources/Schema/XSDWildcardParser.swift
Mihaela Mihaljevic 0d3fbf1b0a fix(schema): intersect a complex type's attribute wildcard sources; invalid-instances-accepted 94 to 89 (#147)
A complex type's effective {attribute wildcard} is the intersection of the
anyAttribute wildcards it draws from its own declaration and each referenced
attribute group (XSD 1.0 cos-aw-intersect), not their union. An attribute is
admitted only if every source admits it, so two attribute groups constraining
disjoint namespaces admit nothing and an attribute in either is rejected.

attributeWildcard(under:_:visited:) combined the sources with Wildcard.union,
which let through an attribute that only one source allowed. It now uses a new
Wildcard.intersection covering the namespace cases (##any identity, enumerated
set intersection, enumerated-with-##other exclusion via exclude(_:otherThan:)).
A type with a single anyAttribute source is unchanged (intersection with the
nil identity is the lone wildcard), and a complexContent extension's union with
its base wildcard (XSDComplexContent) is untouched. The inexpressible
##other/##other-different-target and mismatched processContents cases resolve to
the less restrictive result, so the change never over-rejects.

XSTS invalid-instances-accepted 94 to 89, no false positive (valid-instances-
rejected and valid-schemas-rejected held at 0 across the whole corpus), no other
bucket moved. An adversarial review exercised nested groups, ##other/##local and
cross-import namespace cases, processContents, extension-vs-restriction, and
single-source invariance, and found no new over-rejection.
2026-06-18 11:21:32 +02:00

67 lines
3.4 KiB
Swift

extension PureXML.Schema.XSDParser {
/// The effective `xs:anyAttribute` wildcard declared under `node`, including
/// any `anyAttribute` on nested `attributeGroup` references.
static func attributeWildcard(
under node: XSDTree,
_ context: PureXML.Schema.XSDContext,
visited: Set<String> = [],
) -> PureXML.Schema.Wildcard? {
// A complex type's (or attribute group's) effective {attribute wildcard} is
// the INTERSECTION of the wildcards it draws from its own `anyAttribute` and
// from every referenced attribute group (XSD 1.0 `cos-aw-intersect`): an
// attribute is admitted only if every source admits it. (An extension's
// union with its base wildcard is handled separately, in XSDComplexContent.)
var combined: PureXML.Schema.Wildcard?
if let direct = PureXML.Schema.XSDNode.firstChild(node, named: "anyAttribute").map({ wildcard($0, context) }) {
combined = PureXML.Schema.Wildcard.intersection(combined, direct)
}
for child in PureXML.Schema.XSDNode.elementChildren(node) {
guard PureXML.Schema.XSDNode.localName(child) == "attributeGroup",
let ref = PureXML.Schema.XSDNode.attribute(child, "ref")
else { continue }
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
if visited.contains(name) {
guard context.redefinedAttributeGroups.contains(name),
let base = context.baseAttributeGroups[name],
let found = attributeWildcard(under: base, context, visited: visited)
else { continue }
combined = PureXML.Schema.Wildcard.intersection(combined, found)
continue
}
guard let group = context.attributeGroups[name] else { continue }
let scoped = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(group))
if let found = attributeWildcard(under: group, scoped, visited: visited.union([name])) {
combined = PureXML.Schema.Wildcard.intersection(combined, found)
}
}
return combined
}
/// Parses a wildcard's `namespace` and `processContents` constraints.
static func wildcard(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Wildcard {
let processContents: PureXML.Schema.ProcessContents = switch PureXML.Schema.XSDNode.attribute(node, "processContents") {
case "skip": .skip
case "lax": .lax
default: .strict
}
let namespace = wildcardNamespace(PureXML.Schema.XSDNode.attribute(node, "namespace"), context)
return PureXML.Schema.Wildcard(namespace: namespace, processContents: processContents, targetNamespace: context.targetNamespace)
}
private static func wildcardNamespace(_ value: String?, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.WildcardNamespace {
switch value {
case nil, "##any": return .any
case "##other": return .other
default:
let uris = (value ?? "").split(whereSeparator: \.isWhitespace).map { token -> String in
switch token {
case "##targetNamespace": context.targetNamespace ?? ""
case "##local": ""
default: String(token)
}
}
return .enumerated(Set(uris))
}
}
}