XSD 1.0 §3.11.4 (cvc-identity-constraint.3) requires each key/keyref/unique field to identify a node of simple type. A field that reaches an element declared with no type (the ur-type anyType) or an inline complex type without simple content does not, so the instance is invalid; the reference (libxml2) rejects it identically. PureXML accepted such instances because its identity validator could resolve a field target's type only for a global element reached by a direct-child name, not for a local element reached through a path (the W3C XSTS case idZ010 nests anyType fields several levels deep, e.g. a key field ./b/c where c has no type). A compile-time pass (XSDParser.identityNonSimpleFieldKeys) resolves each field's target element through its selector and field paths, by element-name steps, and records the fields whose declared target is provably non-simple. The validator rejects those at runtime unless the instance gives the node a simple type through xsi:type, since cvc reads the node's governing type (a text-only anyType element with xsi:type="xsd:int" is a valid field). The resolution is conservative: any wildcard, descendant axis, predicate, ref, or named non-builtin type leaves the field unflagged, so a valid field is never rejected. The one residual divergence path, two same-name child elements of differing simplicity in one inline model, is an Element-Declarations-Consistent violation the compiler already rejects. Verified over the full W3C XSTS corpus in release: invalid-instances-accepted 8 -> 7 (idZ010 now rejects), valid-instances-rejected held at 0 across all 14383 groups (no false positive), full suite (1837 tests), wasm, and lint all green. An adversarial critic that read every file and probed for a false positive could not construct one; the failure mode is under-rejection, the safe direction.
401 lines
21 KiB
Swift
401 lines
21 KiB
Swift
private typealias XSDNode = PureXML.Schema.XSDNode
|
|
private typealias XSDContext = PureXML.Schema.XSDContext
|
|
private typealias Particle = PureXML.Schema.Particle
|
|
private typealias ComplexType = PureXML.Schema.ComplexType
|
|
private typealias ValueConstraint = PureXML.Schema.ValueConstraint
|
|
private typealias Compositor = PureXML.Schema.Compositor
|
|
private typealias ElementType = PureXML.Schema.ElementType
|
|
private typealias XSDSimpleParser = PureXML.Schema.XSDSimpleParser
|
|
private typealias BuiltinType = PureXML.Schema.BuiltinType
|
|
private typealias Wildcard = PureXML.Schema.Wildcard
|
|
private typealias Facets = PureXML.Schema.Facets
|
|
private typealias Group = PureXML.Schema.Group
|
|
private typealias SimpleType = PureXML.Schema.SimpleType
|
|
private typealias AttributeUse = PureXML.Schema.AttributeUse
|
|
private typealias XSDCompiled = PureXML.Schema.XSDCompiled
|
|
|
|
extension PureXML.Schema {
|
|
/// Parses an XSD schema document into its global element declarations and its
|
|
/// named-type table, matching the schema vocabulary by local name (so the XML
|
|
/// Schema namespace prefix may be anything). Covers elements, simple and complex
|
|
/// types, model groups, attribute uses, facets, `list`/`union`, and references.
|
|
enum XSDParser {
|
|
static func parse(_ xsd: String, loader: (String) -> String? = { _ in nil }) throws -> XSDCompiled {
|
|
let root = try PureXML.parseTree(xsd)
|
|
guard let schema = XSDNode.elementChildren(root).first(where: { XSDNode.localName($0) == "schema" }) else {
|
|
throw PureXML.Schema.SchemaError.notASchema
|
|
}
|
|
var visited: Set<String> = []
|
|
var rootLocation: String?
|
|
let wrappedLoader: (String) -> String? = { location in
|
|
let content = loader(location)
|
|
if content == xsd {
|
|
rootLocation = location
|
|
}
|
|
return content
|
|
}
|
|
let containerTuples = XSDNode.collectContainers(schema, wrappedLoader, &visited)
|
|
var locationCheckVisited: Set<String> = []
|
|
let failedSchemaReferences = XSDNode.failedSchemaReferences(schema, wrappedLoader, &locationCheckVisited)
|
|
let containers = containerTuples.map(\.tree)
|
|
let derivation = derivationTables(containers, mainTargetNamespace: XSDNode.attribute(schema, "targetNamespace"))
|
|
let containerLocations = containerLocationMap(containerTuples, rootLocation: rootLocation)
|
|
let compositionLoaded = XSDNode.compositionLoaded(from: containerTuples)
|
|
var context = createContext(
|
|
schema: schema,
|
|
containers: containers,
|
|
derivation: derivation,
|
|
containerLocations: containerLocations,
|
|
compositionLoaded: compositionLoaded,
|
|
)
|
|
context.failedSchemaReferences = failedSchemaReferences
|
|
return finishCompile(schema: schema, containers: containers, derivation: derivation, context: &context)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureXML.Schema.XSDParser {
|
|
static func containerLocationMap(
|
|
_ containerTuples: [(location: String?, tree: XSDTree)],
|
|
rootLocation: String?,
|
|
) -> [ObjectIdentifier: String?] {
|
|
var containerLocations: [ObjectIdentifier: String?] = [:]
|
|
for (loc, tree) in containerTuples {
|
|
// The map value is itself `String?`, so the explicit annotation keeps
|
|
// `??` at the `String` level; without it the fallback binds at the
|
|
// outer optional and the compile root (no location) maps to nil.
|
|
let location: String? = loc ?? rootLocation
|
|
containerLocations[ObjectIdentifier(tree)] = location
|
|
}
|
|
return containerLocations
|
|
}
|
|
|
|
static func createContext(
|
|
schema: XSDTree,
|
|
containers: [XSDTree],
|
|
derivation: DerivationTables,
|
|
containerLocations: [ObjectIdentifier: String?],
|
|
compositionLoaded: Bool,
|
|
) -> PureXML.Schema.XSDContext {
|
|
let mainTargetNamespace = XSDNode.attribute(schema, "targetNamespace")
|
|
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: mainTargetNamespace)
|
|
var context = XSDContext(
|
|
simpleTypes: [:],
|
|
attributeGroups: indexByName(allChildren(containers, named: "attributeGroup")),
|
|
groups: indexByName(allChildren(containers, named: "group")),
|
|
targetNamespace: mainTargetNamespace,
|
|
substitutions: filterSubstitutions(
|
|
XSDNode.substitutionMembers(
|
|
containers,
|
|
namespaceMap: namespaceMap,
|
|
mainTargetNamespace: mainTargetNamespace,
|
|
chainBlockedBySubstitution: Set(derivation.nsElementBlock.compactMap { element, block in
|
|
block.contains(.substitution) ? element : nil
|
|
}),
|
|
),
|
|
derivation,
|
|
),
|
|
abstractElements: derivation.abstractElements,
|
|
compositionLoaded: compositionLoaded,
|
|
containerLocations: containerLocations,
|
|
)
|
|
context.blockDefault = XSDNode.attribute(schema, "blockDefault")
|
|
context.elementFormQualified = XSDNode.attribute(schema, "elementFormDefault") == "qualified"
|
|
context.attributeFormQualified = XSDNode.attribute(schema, "attributeFormDefault") == "qualified"
|
|
context.namespaceBindings = XSDNode.namespaceBindings(of: schema)
|
|
context.complexTypeNodes = indexByName(allChildren(containers, named: "complexType"))
|
|
context.globalAttributes = indexByName(allChildren(containers, named: "attribute"))
|
|
context.globalElements = globalElementNodes(containers, namespaceMap: namespaceMap, mainTargetNamespace: mainTargetNamespace)
|
|
let redefinedAttributeGroups = PureXML.Schema.XSDParser.redefinedNames(containers, "attributeGroup")
|
|
let redefinedGroups = PureXML.Schema.XSDParser.redefinedNames(containers, "group")
|
|
let redefinedComplexTypes = PureXML.Schema.XSDParser.redefinedNames(containers, "complexType")
|
|
context.redefinedAttributeGroups = redefinedAttributeGroups
|
|
context.redefinedGroups = redefinedGroups
|
|
context.redefinedComplexTypes = redefinedComplexTypes
|
|
context.baseAttributeGroups = baseNamedComponents(containers, named: "attributeGroup")
|
|
context.baseGroups = baseNamedComponents(containers, named: "group")
|
|
context.baseComplexTypeNodes = baseNamedComponents(containers, named: "complexType")
|
|
context.chameleonTargetNamespaces = chameleonTargetNamespaces(containers, locations: containerLocations)
|
|
return context
|
|
}
|
|
|
|
private static func chameleonTargetNamespaces(
|
|
_ containers: [XSDTree],
|
|
locations: [ObjectIdentifier: String?],
|
|
) -> [ObjectIdentifier: String] {
|
|
var map: [ObjectIdentifier: String] = [:]
|
|
let schemas = containers.filter { XSDNode.localName($0) == "schema" }
|
|
for included in schemas {
|
|
let declared = XSDNode.attribute(included, "targetNamespace")
|
|
guard declared == nil || declared?.isEmpty == true else { continue }
|
|
guard let includedLocation = locations[ObjectIdentifier(included)] ?? nil else { continue }
|
|
let includedBase = includedLocation.split(separator: "/").last.map(String.init) ?? includedLocation
|
|
for parent in schemas where parent !== included {
|
|
guard let parentNamespace = XSDNode.attribute(parent, "targetNamespace"), !parentNamespace.isEmpty else { continue }
|
|
for child in XSDNode.elementChildren(parent) where XSDNode.localName(child) == "include" {
|
|
let href = XSDNode.attribute(child, "schemaLocation") ?? ""
|
|
if href == includedBase || includedLocation.hasSuffix(href) {
|
|
map[ObjectIdentifier(included)] = parentNamespace
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return map
|
|
}
|
|
|
|
private struct CompiledBuild {
|
|
var elements: [String: ElementType]
|
|
var types: [String: ElementType]
|
|
var globalAttributes: [String: AttributeUse]
|
|
var containers: [XSDTree]
|
|
var derivation: DerivationTables
|
|
var context: XSDContext
|
|
var identityFieldTypes: [String: SimpleType]
|
|
var identityFieldConstraints: [String: ValueConstraint]
|
|
var identityNonSimpleFields: Set<String>
|
|
}
|
|
|
|
static func finishCompile(
|
|
schema: XSDTree,
|
|
containers: [XSDTree],
|
|
derivation: DerivationTables,
|
|
context: inout PureXML.Schema.XSDContext,
|
|
) -> PureXML.Schema.XSDCompiled {
|
|
for error in PureXML.Validation.SchemaCompile.preCompileErrors(schema: schema, context: context, containers: containers) {
|
|
context.diagnostics.report(error)
|
|
}
|
|
seedElementNamespaces(containers, &context)
|
|
var types = namedTypes(containers, into: &context)
|
|
let elements = globalElements(containers, &context, into: &types)
|
|
let globalAttributes = globalAttributeUses(containers, context)
|
|
let postCompileDocument = PureXML.Schema.SchemaCompileContext(
|
|
schema: schema,
|
|
context: context,
|
|
containers: containers,
|
|
derivation: derivation,
|
|
globalElements: elements,
|
|
namedTypes: types,
|
|
)
|
|
for error in PureXML.Validation.SchemaCompile.postCompileErrors(in: postCompileDocument) {
|
|
context.diagnostics.report(error)
|
|
}
|
|
return buildCompiled(CompiledBuild(
|
|
elements: elements,
|
|
types: types,
|
|
globalAttributes: globalAttributes,
|
|
containers: containers,
|
|
derivation: derivation,
|
|
context: context,
|
|
identityFieldTypes: identityFieldTypes(containers, context),
|
|
identityFieldConstraints: identityFieldConstraints(containers, context),
|
|
identityNonSimpleFields: identityNonSimpleFieldKeys(containers, context),
|
|
))
|
|
}
|
|
|
|
private static func buildCompiled(_ build: CompiledBuild) -> XSDCompiled {
|
|
let (nillable, elementConstraints) = elementMetadata(build.containers)
|
|
return XSDCompiled(
|
|
elements: build.elements,
|
|
types: build.types,
|
|
constraints: identityConstraints(build.containers),
|
|
identityFieldTypes: build.identityFieldTypes,
|
|
identityFieldConstraints: build.identityFieldConstraints,
|
|
identityNonSimpleFields: build.identityNonSimpleFields,
|
|
nillableElements: nillable,
|
|
elementConstraints: elementConstraints,
|
|
abstractTypes: build.derivation.abstractTypes,
|
|
abstractElements: build.derivation.abstractElements,
|
|
typeBlock: build.derivation.typeBlock,
|
|
elementBlock: build.derivation.elementBlock,
|
|
typeDerivation: build.derivation.typeDerivation,
|
|
nsAbstractTypes: build.derivation.nsAbstractTypes,
|
|
nsTypeBlock: build.derivation.nsTypeBlock,
|
|
nsElementBlock: build.derivation.nsElementBlock,
|
|
nsTypeDerivation: build.derivation.nsTypeDerivation,
|
|
nsUnionMembers: build.derivation.nsUnionMembers,
|
|
typeFinal: build.derivation.typeFinal,
|
|
targetNamespace: build.context.targetNamespace,
|
|
schemaErrors: build.context.diagnostics.deduplicated,
|
|
globalAttributes: build.globalAttributes,
|
|
)
|
|
}
|
|
|
|
private static func namedTypes(_ containers: [XSDTree], into context: inout XSDContext) -> [String: ElementType] {
|
|
var types: [String: ElementType] = [:]
|
|
for container in containers {
|
|
let kind = XSDNode.localName(container)
|
|
guard kind == "schema" || kind == "redefine" else { continue }
|
|
var containerContext = scopedContext(from: context, container: container)
|
|
let simpleTypeNodes = kind == "redefine"
|
|
? XSDNode.elementChildren(container).filter { XSDNode.localName($0) == "simpleType" }
|
|
: XSDNode.children(container, named: "simpleType")
|
|
resolveSimpleTypes(simpleTypeNodes, into: &containerContext)
|
|
for (name, simple) in containerContext.simpleTypes {
|
|
context.simpleTypes[name] = simple
|
|
let compiled = ElementType.simple(simple)
|
|
let key = typeDeclarationKey(name, namespaceURI: containerContext.targetNamespace)
|
|
types[key] = compiled
|
|
types[name] = compiled
|
|
}
|
|
let complexTypeNodes = kind == "redefine"
|
|
? XSDNode.elementChildren(container).filter { XSDNode.localName($0) == "complexType" }
|
|
: XSDNode.children(container, named: "complexType")
|
|
for node in complexTypeNodes {
|
|
guard let name = XSDNode.attribute(node, "name") else { continue }
|
|
let compiled = ElementType.complex(complexType(node, containerContext.visitingType(name)))
|
|
let key = typeDeclarationKey(name, namespaceURI: containerContext.targetNamespace)
|
|
types[key] = compiled
|
|
types[name] = compiled
|
|
}
|
|
}
|
|
return types
|
|
}
|
|
|
|
private static func scopedContext(from context: XSDContext, container: XSDTree) -> XSDContext {
|
|
context.scoped(for: container)
|
|
}
|
|
|
|
/// Records each global element's target namespace before types are compiled,
|
|
/// so substitution-group expansion during particle compilation sees the right
|
|
/// namespace for imported members (for example `add:salutation` in ipo6).
|
|
private static func seedElementNamespaces(_ containers: [XSDTree], _ context: inout XSDContext) {
|
|
for container in containers where XSDNode.localName(container) == "schema" {
|
|
let containerContext = scopedContext(from: context, container: container)
|
|
for node in XSDNode.children(container, named: "element") {
|
|
guard XSDNode.attribute(node, "name") != nil, XSDNode.attribute(node, "ref") == nil else { continue }
|
|
let name = XSDNode.attribute(node, "name") ?? ""
|
|
context.elementNamespaces[name] = containerContext.targetNamespace
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func globalElements(
|
|
_ containers: [XSDTree],
|
|
_ context: inout XSDContext,
|
|
into types: inout [String: ElementType],
|
|
) -> [String: ElementType] {
|
|
var elements: [String: ElementType] = [:]
|
|
for container in containers where XSDNode.localName(container) == "schema" {
|
|
let containerContext = scopedContext(from: context, container: container)
|
|
for node in XSDNode.children(container, named: "element") {
|
|
guard XSDNode.attribute(node, "name") != nil, XSDNode.attribute(node, "ref") == nil else { continue }
|
|
let name = XSDNode.attribute(node, "name") ?? ""
|
|
context.elementNamespaces[name] = containerContext.targetNamespace
|
|
let type = elementType(node, containerContext)
|
|
elements[name] = type
|
|
types[elementKey(name)] = type
|
|
let qualified = PureXML.Model.QualifiedName(localName: name, namespaceURI: containerContext.targetNamespace)
|
|
types[elementDeclarationKey(qualified)] = type
|
|
}
|
|
}
|
|
return elements
|
|
}
|
|
|
|
private static func globalAttributeUses(
|
|
_ containers: [XSDTree],
|
|
_ context: XSDContext,
|
|
) -> [String: AttributeUse] {
|
|
var attributes: [String: AttributeUse] = [:]
|
|
for container in containers where XSDNode.localName(container) == "schema" {
|
|
let containerContext = scopedContext(from: context, container: container)
|
|
for node in XSDNode.children(container, named: "attribute") {
|
|
guard XSDNode.attribute(node, "name") != nil, XSDNode.attribute(node, "ref") == nil,
|
|
var use = attributeUse(node, containerContext)
|
|
else { continue }
|
|
if let target = containerContext.targetNamespace, !target.isEmpty {
|
|
use.name = PureXML.Model.QualifiedName(localName: use.name.localName, namespaceURI: target)
|
|
let explicitTarget = XSDNode.attribute(container, "targetNamespace")
|
|
if explicitTarget == nil || explicitTarget?.isEmpty == true {
|
|
use.chameleonUnprefixed = true
|
|
}
|
|
}
|
|
attributes[attributeDeclarationKey(use.name)] = use
|
|
}
|
|
}
|
|
return attributes
|
|
}
|
|
|
|
private static func allChildren(_ containers: [XSDTree], named name: String) -> [XSDTree] {
|
|
containers.flatMap { XSDNode.children($0, named: name) }
|
|
}
|
|
|
|
private static func globalElementNodes(
|
|
_ containers: [XSDTree],
|
|
namespaceMap: [Int: String?],
|
|
mainTargetNamespace: String?,
|
|
) -> [String: XSDTree] {
|
|
var index: [String: XSDTree] = [:]
|
|
for containerIndex in containers.indices {
|
|
let container = containers[containerIndex]
|
|
guard XSDNode.localName(container) == "schema" else { continue }
|
|
let namespace = namespaceMap[containerIndex] ?? mainTargetNamespace
|
|
for node in XSDNode.children(container, named: "element") {
|
|
guard let name = XSDNode.attribute(node, "name"), XSDNode.attribute(node, "ref") == nil else { continue }
|
|
index[derivationKey(name, in: namespace)] = node
|
|
}
|
|
}
|
|
return index
|
|
}
|
|
|
|
/// Named component definitions from every container except `redefine`
|
|
/// overlays, so a redefinition's self-reference resolves to the included
|
|
/// definition rather than the overlay being built.
|
|
private static func baseNamedComponents(_ containers: [XSDTree], named kind: String) -> [String: XSDTree] {
|
|
var index: [String: XSDTree] = [:]
|
|
for container in containers where XSDNode.localName(container) != "redefine" {
|
|
for node in XSDNode.children(container, named: kind) {
|
|
guard let name = XSDNode.attribute(node, "name") else { continue }
|
|
index[name] = node
|
|
}
|
|
}
|
|
return index
|
|
}
|
|
|
|
/// Resolves the named simple types into `context` over repeated passes:
|
|
/// named simple types may restrict or list one another regardless of
|
|
/// document order, so each pass sees the types the previous one resolved. A
|
|
/// dependency chain of length n settles in at most n passes.
|
|
private static func resolveSimpleTypes(_ nodes: [XSDTree], into context: inout XSDContext) {
|
|
let named = nodes.filter { XSDNode.attribute($0, "name") != nil }
|
|
let nodeByName = simpleTypeNodesByName(named)
|
|
// Resolve in dependency order (base before derived) so each type is compiled once,
|
|
// not re-resolved N times by the former fixpoint; the resolved set is the same.
|
|
for name in simpleTypeResolutionOrder(named, nodeByName: nodeByName) {
|
|
guard let node = nodeByName[name] else { continue }
|
|
context.simpleTypes[name] = XSDSimpleParser.simpleType(node, context)
|
|
}
|
|
}
|
|
|
|
// MARK: Complex types
|
|
|
|
static func complexType(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.ComplexType {
|
|
let mixed = XSDNode.attribute(node, "mixed").map { $0 == "true" || $0 == "1" } ?? false
|
|
let attributes = attributeUses(under: node, context)
|
|
if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") {
|
|
let inner = derivation(simpleContent)
|
|
let wildcard = inner.flatMap { attributeWildcard(under: $0, context) }
|
|
let derived = inner.map { PureXML.Schema.XSDParser.simpleContentAttributes(under: $0, context) } ?? []
|
|
return ComplexType(
|
|
attributes: attributes + derived,
|
|
attributeWildcard: wildcard,
|
|
content: .simpleContent(simpleContentType(simpleContent, context)),
|
|
)
|
|
}
|
|
if let complexContent = XSDNode.firstChild(node, named: "complexContent"), let inner = derivation(complexContent) {
|
|
return complexContentType(inner, mixed: mixed || XSDNode.attribute(complexContent, "mixed").map { $0 == "true" || $0 == "1" } ?? false, context)
|
|
}
|
|
let wildcard = attributeWildcard(under: node, context)
|
|
guard let particle = modelGroup(in: node, context) else {
|
|
// A mixed type with no content model still permits character data
|
|
// (just no child elements): mixed content over an empty particle,
|
|
// not the empty content type, which forbids text.
|
|
guard mixed else {
|
|
return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: .empty)
|
|
}
|
|
let emptyParticle = Particle(term: .group(.init(compositor: .sequence, particles: [])))
|
|
return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: .mixed(emptyParticle))
|
|
}
|
|
return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: mixed ? .mixed(particle) : .elementOnly(particle))
|
|
}
|
|
}
|