Particle Valid (Restriction) now requires a restricting element's {disallowed
substitutions} to be a superset of the base element's: a restriction may block
more substitutions, never fewer. The element particle (Term.element) carries a
block set, populated from the element's block attribute or the schema's
blockDefault.
XSTS invalid-schemas-accepted 179 to 163, no other bucket moved.
378 lines
19 KiB
Swift
378 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)
|
|
let containers = containerTuples.map(\.tree)
|
|
let derivation = derivationTables(containers)
|
|
try checkRedefine(containers)
|
|
try checkAllGroups(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,
|
|
)
|
|
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)
|
|
}
|
|
}
|