PureXML/Sources/Schema/XSDParser.swift
Mihaela Mihaljevic 4def6a2c07 refactor(schema): redefine self-derivation as a Validation; drop the redundant all-group throw (#161)
Continues incorporating every schema-validity check into the Validator framework
(Rules/universal/validation-rules.md) rather than ad-hoc parse-pipeline throws.

- checkRedefine (src-redefine.5: a redefined type restricts or extends itself)
  becomes the pre-compile Validation redefinitionsDeriveFromThemselves;
  redefineDerivationErrors returns findings instead of throwing.

- checkAllGroups is removed entirely: every check it made is already produced by
  Validations -- "a member must be an element" by schemaStructureValid
  (allowedChildren[all]), and the occurrence limits by allGroupLimitedErrors
  (cos-all-limited.2). Verified redundant and not load-bearing: an empirical
  probe with the calls removed left invalid-schemas-accepted at 81 unchanged,
  the fuzz suite's "compile and validation never crash or hang" still passes
  (the NFA builder's own guards hold), and false positives stayed 0.

compileRule is now internal so composition Validations can live in their own
file (SchemaCompositionValidations.swift). Both new pre-compile rules are pinned
in ValidationConfigurationTests. The pre-existing public SchemaError.invalidAllGroup
case is kept (released API) though no longer thrown. No bucket change
(invalid-schemas-accepted 81, valid-schemas-rejected 0); full suite + fuzz pass.
2026-06-17 23:49:30 +02:00

379 lines
19 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. Matches the schema vocabulary by local name, so the XML
/// Schema namespace prefix may be anything. Supports global and local element
/// declarations, named and inline simple and complex types, `sequence`,
/// `choice`, and `all` model groups with occurrence, attribute uses, simple
/// content, the full facet set, `list` and `union` simple types, named
/// attribute groups and model groups, and element and group 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)
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 {
containerLocations[ObjectIdentifier(tree)] = loc ?? rootLocation
}
return containerLocations
}
static func createContext(
schema: XSDTree,
containers: [XSDTree],
derivation: DerivationTables,
containerLocations: [ObjectIdentifier: String?],
compositionLoaded: Bool,
) -> PureXML.Schema.XSDContext {
var context = XSDContext(
simpleTypes: [:],
attributeGroups: indexByName(allChildren(containers, named: "attributeGroup")),
groups: indexByName(allChildren(containers, named: "group")),
targetNamespace: XSDNode.attribute(schema, "targetNamespace"),
substitutions: filterSubstitutions(XSDNode.substitutionMembers(containers), 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"))
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]
}
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),
))
}
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,
nillableElements: nillable,
elementConstraints: elementConstraints,
abstractTypes: build.derivation.abstractTypes,
abstractElements: build.derivation.abstractElements,
typeBlock: build.derivation.typeBlock,
elementBlock: build.derivation.elementBlock,
typeDerivation: build.derivation.typeDerivation,
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) }
}
/// 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 }
for _ in named.indices {
for node in named {
if let name = XSDNode.attribute(node, "name") {
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))
}
private static func simpleContentType(_ node: XSDTree, _ context: XSDContext) -> SimpleType {
guard let inner = derivation(node) else { return SimpleType(base: .string) }
let rawBase = XSDNode.attribute(inner, "base") ?? "string"
let baseName = XSDNode.stripPrefix(rawBase)
let bindings = PureXML.Schema.XSDParser.namespaceBindingsInScope(of: inner, defaultBindings: context.namespaceBindings)
let uri = XSDNode.referenceNamespace(rawBase, bindings)
let base: BuiltinType
let inheritedFacets: Facets
if uri == PureXML.Schema.XSDParser.xsdNamespace {
base = BuiltinType(rawValue: baseName) ?? .string
inheritedFacets = Facets()
} else {
base = context.simpleTypes[baseName]?.base ?? .string
inheritedFacets = context.simpleTypes[baseName]?.facets ?? Facets()
}
var facets = inheritedFacets
XSDSimpleParser.applyFacets(inner, into: &facets)
return SimpleType(base: base, facets: facets)
}
}