PureXML/Sources/Schema/XSDComplexContent.swift
Mihaela Mihaljevic be8a39394d fix(schema): advance XSTS valid-instances-rejected to 4 (#146)
Consolidates the in-progress #146 work; valid-instances-rejected reaches 4
on the 2006-11-06 XSTS archive (buckets now 1/262/4/147).

Regex: match Unicode scalars rather than grapheme clusters; the empty
pattern matches only the empty string; pin the category repertoire to
Unicode 3.2, the version the corpus is consistent at, so code points added
in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions
(the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a
3.2 assignment table derived from DerivedAge, replacing the old Unicode31
files.

Content model: assess each child against the particle the content automaton
matched, not a by-name lookup (#180). Two same-named particles keep their
own fixed values, and an element matching a skip wildcard is no longer
validated against its global declaration. Selection is made reproducible
when an ambiguous schema slips past the UPA check.

XSTS runner: compile and merge every schemaDocument in a schemaTest, not
just the first; this also surfaced one previously-masked invalid-instance
acceptance (recorded in the baselines).

Also folds in attribute-group anyAttribute inheritance, extension wildcard
union, namespace-qualified type tables for multi-document
xsi:schemaLocation, per-container elementFormDefault, strict and lax
attribute wildcard validation, built-in type resolution, and xs:redefine
simpleType overrides.
2026-06-16 22:22:05 +02:00

210 lines
10 KiB
Swift

private typealias XSDComplexType = PureXML.Schema.ComplexType
private typealias XSDContentType = PureXML.Schema.ContentType
private typealias XSDParticle = PureXML.Schema.Particle
private typealias XSDGroup = PureXML.Schema.Group
private typealias XSDContentNode = PureXML.Schema.XSDNode
extension PureXML.Schema.XSDParser {
/// Attribute uses contributed by a `simpleContent` extension or restriction,
/// including those inherited from a complex base type.
static func simpleContentAttributes(under derivationNode: XSDTree, _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.AttributeUse] {
let ownAttributes = attributeUses(under: derivationNode, context)
let derivationType = XSDContentNode.localName(derivationNode)
if derivationType == "extension", let base = resolvedBase(derivationNode, context) {
return base.attributes + ownAttributes
}
if derivationType == "restriction" {
let allowedOwnAttributes = ownAttributes.filter { !isProhibited($0.name, under: derivationNode, context) }
if let base = resolvedBase(derivationNode, context) {
var attributes: [PureXML.Schema.AttributeUse] = []
for baseAttr in base.attributes {
if isProhibited(baseAttr.name, under: derivationNode, context) { continue }
if let derived = allowedOwnAttributes.first(where: { $0.name == baseAttr.name }) {
attributes.append(derived)
} else {
attributes.append(baseAttr)
}
}
for ownAttr in allowedOwnAttributes where !base.attributes.contains(where: { $0.name == ownAttr.name }) {
attributes.append(ownAttr)
}
return attributes
}
return allowedOwnAttributes
}
return ownAttributes
}
/// Builds the effective complex type of a `complexContent` derivation. An
/// extension composes the base type's attributes and content with its own
/// (the base's content first, then the extension's, in sequence); a
/// restriction states the full restricted model and replaces the base.
///
/// Note: a restriction's model is taken as given. It is NOT verified to be a
/// structurally valid subset of the base ("Particle Valid (Restriction)" in
/// XSD 1.0), which is a much larger algorithm; an unfaithful restriction is
/// accepted.
static func complexContentType(_ derivationNode: XSDTree, mixed: Bool, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.ComplexType {
let ownAttributes = attributeUses(under: derivationNode, context)
let ownWildcard = attributeWildcard(under: derivationNode, context)
let ownParticle = modelGroup(in: derivationNode, context)
let derivationType = XSDContentNode.localName(derivationNode)
if derivationType == "extension", let base = resolvedBase(derivationNode, context) {
return XSDComplexType(
attributes: base.attributes + ownAttributes,
attributeWildcard: PureXML.Schema.Wildcard.union(ownWildcard, base.attributeWildcard),
content: extendedContent(base: base.content, own: ownParticle, mixed: mixed),
)
} else if derivationType == "restriction" {
let allowedOwnAttributes = ownAttributes.filter { !isProhibited($0.name, under: derivationNode, context) }
let finalAttributes: [PureXML.Schema.AttributeUse]
if let base = resolvedBase(derivationNode, context) {
var attributes: [PureXML.Schema.AttributeUse] = []
for baseAttr in base.attributes {
if isProhibited(baseAttr.name, under: derivationNode, context) {
continue
}
if let derived = allowedOwnAttributes.first(where: { $0.name == baseAttr.name }) {
attributes.append(derived)
} else {
attributes.append(baseAttr)
}
}
for ownAttr in allowedOwnAttributes {
guard !base.attributes.contains(where: { $0.name == ownAttr.name }) else { continue }
attributes.append(ownAttr)
}
finalAttributes = attributes
} else {
finalAttributes = allowedOwnAttributes
}
return XSDComplexType(
attributes: finalAttributes,
attributeWildcard: ownWildcard,
content: content(ownParticle, mixed: mixed),
)
}
return XSDComplexType(
attributes: ownAttributes,
attributeWildcard: ownWildcard,
content: content(ownParticle, mixed: mixed),
)
}
private static func isProhibited(_ name: PureXML.Model.QualifiedName, under node: XSDTree, _ context: PureXML.Schema.XSDContext, visited: Set<String> = []) -> Bool {
for child in XSDContentNode.elementChildren(node) {
switch XSDContentNode.localName(child) {
case "attribute":
if let attrName = attributeName(child, context), attrName == name {
if XSDContentNode.attribute(child, "use") == "prohibited" {
return true
}
}
case "attributeGroup":
guard let ref = XSDContentNode.attribute(child, "ref") else { break }
let refName = XSDContentNode.stripPrefix(ref)
if attributeGroupProhibits(name, refName: refName, under: node, context, visited: visited) {
return true
}
default:
break
}
}
return false
}
private static func attributeGroupProhibits(
_ name: PureXML.Model.QualifiedName,
refName: String,
under _: XSDTree,
_ context: PureXML.Schema.XSDContext,
visited: Set<String>,
) -> Bool {
if visited.contains(refName) {
guard context.redefinedAttributeGroups.contains(refName),
let base = context.baseAttributeGroups[refName] else { return false }
return isProhibited(name, under: base, context, visited: visited)
}
guard let group = context.attributeGroups[refName] else { return false }
return isProhibited(name, under: group, context, visited: visited.union([refName]))
}
private static func attributeName(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Model.QualifiedName? {
if let ref = XSDContentNode.attribute(node, "ref") {
let refName = XSDContentNode.stripPrefix(ref)
let namespace = XSDContentNode.referenceNamespace(ref, context.namespaceBindings)
return PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace)
}
guard let name = XSDContentNode.attribute(node, "name") else { return nil }
let qualified = XSDContentNode.attribute(node, "form") == "qualified"
|| (XSDContentNode.attribute(node, "form") == nil && context.attributeFormQualified)
return PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? context.targetNamespace : nil)
}
/// The base complex type a derivation extends, parsed from its definition
/// node, or nil when the base is unknown (a built-in or undeclared type) or
/// would form a derivation cycle.
private static func resolvedBase(_ derivationNode: XSDTree, _ context: PureXML.Schema.XSDContext) -> XSDComplexType? {
guard let baseName = XSDContentNode.attribute(derivationNode, "base").map(XSDContentNode.stripPrefix) else {
return nil
}
guard let baseNode = baseTypeNode(named: baseName, context) else { return nil }
let scoped = context.scoped(for: XSDContentNode.schemaOwner(baseNode))
return complexType(baseNode, scoped.visitingType(baseName))
}
/// The schema node a complex type derives from: the redefined base when this
/// type is the redefining occurrence of `baseName`, otherwise the ordinary
/// declaration. Returns nil when resolving would form a derivation cycle.
private static func baseTypeNode(named baseName: String, _ context: PureXML.Schema.XSDContext) -> XSDTree? {
if let redefined = redefinedBaseNode(named: baseName, context) { return redefined }
guard !context.visitingTypes.contains(baseName) else { return nil }
return context.complexTypeNodes[baseName]
}
private static func redefinedBaseNode(named baseName: String, _ context: PureXML.Schema.XSDContext) -> XSDTree? {
guard context.redefinedComplexTypes.contains(baseName),
context.visitingTypes.contains(baseName),
let baseNode = context.baseComplexTypeNodes[baseName]
else { return nil }
return baseNode
}
/// A particle wrapped as element-only or mixed content, or empty content when
/// there is no particle.
private static func content(_ particle: XSDParticle?, mixed: Bool) -> XSDContentType {
guard let particle else { return mixed ? .mixed(emptyParticle) : .empty }
return mixed ? .mixed(particle) : .elementOnly(particle)
}
/// The content of an extension: the base content's particle followed by the
/// extension's own particle, in a sequence. Mixed if either side is mixed.
private static func extendedContent(base: XSDContentType, own: XSDParticle?, mixed: Bool) -> XSDContentType {
let baseMixed = if case .mixed = base { true } else { false }
let combined = combine(particle(of: base), own)
return content(combined, mixed: mixed || baseMixed)
}
private static func particle(of content: XSDContentType) -> XSDParticle? {
switch content {
case let .elementOnly(particle), let .mixed(particle): particle
case .empty, .simpleContent: nil
}
}
private static func combine(_ first: XSDParticle?, _ second: XSDParticle?) -> XSDParticle? {
switch (first, second) {
case let (first?, second?):
XSDParticle(term: .group(XSDGroup(compositor: .sequence, particles: [first, second])))
case let (first?, nil): first
case let (nil, second): second
}
}
private static var emptyParticle: XSDParticle {
XSDParticle(minOccurs: 0, maxOccurs: 1, term: .group(XSDGroup(compositor: .sequence, particles: [])))
}
}