A complex type's or attribute group's attribute-use set (flattened through attributeGroup references) may not contain two distinct declarations with the same name and namespace (ct-props-correct.4 / ag-props-correct.2), nor more than one attribute whose type is or derives from xs:ID (ct-props-correct.5). Declarations are deduplicated by the identity of their declaring node, so the same declaration reached by two attribute-group paths (a diamond) is one use, not a clash. Names carry their resolved namespace (form / attributeFormDefault for locals, the prefix binding for refs), so a local attribute and a reference to a same-local-name attribute in an imported namespace do not collide. The check runs after named simple types are resolved, so a named user type derived from xs:ID is recognized, not just the built-in and inline forms. XSTS: invalid schemas accepted 505 to 495 (attQ008/009/012/013, attgD006/007/ 009/017, ctM003/004 now rejected), no other bucket raised. An adversarial critic caught a false positive on same-local-name-different-namespace (fixed by resolving the ref namespace) and a missed named-user-ID type (fixed by ordering the check after type resolution). Disclosed under-rejections: the set inherited through extension is not composed in; a duplicate needing an unprefixed reference with no default namespace is not detected.
401 lines
21 KiB
Swift
401 lines
21 KiB
Swift
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> = []
|
|
let containers = XSDNode.collectContainers(schema, loader, &visited)
|
|
let derivation = derivationTables(containers)
|
|
// checkRedefine and checkAllGroups examine the raw schema source, the
|
|
// schema-compilation analog of well-formedness, so they stay throws;
|
|
// the model-level consistency checks (final, restriction subsets) are
|
|
// Validation rules collected by Schema.Document.
|
|
try checkRedefine(containers)
|
|
try checkAllGroups(containers)
|
|
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,
|
|
)
|
|
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"))
|
|
for error in consistencyErrors(schema, context, containers) {
|
|
context.diagnostics.report(error)
|
|
}
|
|
var types = namedTypes(containers, into: &context)
|
|
let elements = globalElements(containers, context, into: &types)
|
|
// Attribute-use uniqueness / single-ID run here, after `namedTypes`
|
|
// populated `simpleTypes`, so a named user type derived from `xs:ID` counts.
|
|
for error in referenceErrors(schema, in: context, elements: elements) + attributeUseErrors(containers, context) {
|
|
context.diagnostics.report(error)
|
|
}
|
|
let (nillable, elementConstraints) = elementMetadata(containers)
|
|
return XSDCompiled(
|
|
elements: elements,
|
|
types: types,
|
|
constraints: identityConstraints(containers),
|
|
nillableElements: nillable,
|
|
elementConstraints: elementConstraints,
|
|
abstractTypes: derivation.abstractTypes,
|
|
abstractElements: derivation.abstractElements,
|
|
typeBlock: derivation.typeBlock,
|
|
elementBlock: derivation.elementBlock,
|
|
typeDerivation: derivation.typeDerivation,
|
|
typeFinal: derivation.typeFinal,
|
|
targetNamespace: context.targetNamespace,
|
|
schemaErrors: context.diagnostics.deduplicated,
|
|
)
|
|
}
|
|
|
|
/// The named-type table: the global simple types resolved into `context`,
|
|
/// then each global complex type compiled and added by name.
|
|
private static func namedTypes(_ containers: [XSDTree], into context: inout XSDContext) -> [String: ElementType] {
|
|
resolveSimpleTypes(allChildren(containers, named: "simpleType"), into: &context)
|
|
var types: [String: ElementType] = context.simpleTypes.mapValues(ElementType.simple)
|
|
for node in allChildren(containers, named: "complexType") {
|
|
if let name = XSDNode.attribute(node, "name") {
|
|
types[name] = .complex(complexType(node, context))
|
|
}
|
|
}
|
|
return types
|
|
}
|
|
|
|
/// The global element declarations, also registering each under a reserved
|
|
/// `element:name` key in `types` so an `xs:element ref="name"` resolves to
|
|
/// this declaration's type (a colon keeps it out of the NCName-only type
|
|
/// namespace), letting forward and recursive refs resolve at validation time.
|
|
private static func globalElements(
|
|
_ containers: [XSDTree],
|
|
_ context: XSDContext,
|
|
into types: inout [String: ElementType],
|
|
) -> [String: ElementType] {
|
|
var elements: [String: ElementType] = [:]
|
|
for node in allChildren(containers, named: "element") where XSDNode.attribute(node, "name") != nil {
|
|
let name = XSDNode.attribute(node, "name") ?? ""
|
|
let type = elementType(node, context)
|
|
elements[name] = type
|
|
types[elementKey(name)] = type
|
|
}
|
|
return elements
|
|
}
|
|
|
|
private static func allChildren(_ containers: [XSDTree], named name: String) -> [XSDTree] {
|
|
containers.flatMap { XSDNode.children($0, named: name) }
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static func elementKey(_ name: String) -> String {
|
|
"element:\(name)"
|
|
}
|
|
|
|
private static func indexByName(_ nodes: [XSDTree]) -> [String: XSDTree] {
|
|
var index: [String: XSDTree] = [:]
|
|
for node in nodes {
|
|
if let name = XSDNode.attribute(node, "name") { index[name] = node }
|
|
}
|
|
return index
|
|
}
|
|
|
|
// MARK: Element and type references
|
|
|
|
/// The derivation identity of an element's type: the resolved local name of
|
|
/// its `type` reference, `anyType` when the type is absent (the implied type),
|
|
/// or nil when the type is an inline anonymous definition (no name to derive
|
|
/// against).
|
|
static func elementTypeName(_ node: XSDTree) -> String? {
|
|
if let typeName = XSDNode.attribute(node, "type") {
|
|
return XSDNode.stripPrefix(typeName)
|
|
}
|
|
if XSDNode.firstChild(node, named: "simpleType") != nil || XSDNode.firstChild(node, named: "complexType") != nil {
|
|
return nil
|
|
}
|
|
return "anyType"
|
|
}
|
|
|
|
private static func elementType(_ node: XSDTree, _ context: XSDContext) -> ElementType {
|
|
if let typeName = XSDNode.attribute(node, "type") {
|
|
return typeReference(typeName)
|
|
}
|
|
if let inline = XSDNode.firstChild(node, named: "simpleType") {
|
|
return .simple(XSDSimpleParser.simpleType(inline, context))
|
|
}
|
|
if let inline = XSDNode.firstChild(node, named: "complexType") {
|
|
return .complex(complexType(inline, context))
|
|
}
|
|
return .complex(anyType)
|
|
}
|
|
|
|
private static func typeReference(_ typeName: String) -> ElementType {
|
|
let local = XSDNode.stripPrefix(typeName)
|
|
if let builtin = BuiltinType(rawValue: local) {
|
|
return .simple(SimpleType(base: builtin))
|
|
}
|
|
if let item = XSDSimpleParser.listBuiltinItem(local) {
|
|
return .simple(.list(item: SimpleType(base: item)))
|
|
}
|
|
return .typeReference(local)
|
|
}
|
|
|
|
private static var anyType: ComplexType {
|
|
ComplexType(
|
|
attributeWildcard: Wildcard(processContents: .skip),
|
|
content: .mixed(Particle(minOccurs: 0, maxOccurs: nil, term: .wildcard(Wildcard(processContents: .skip)))),
|
|
)
|
|
}
|
|
|
|
// MARK: Complex types
|
|
|
|
static func complexType(_ node: XSDTree, _ context: XSDContext) -> ComplexType {
|
|
let mixed = XSDNode.attribute(node, "mixed") == "true"
|
|
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 extra = inner.map { attributeUses(under: $0, context) } ?? []
|
|
return ComplexType(attributes: attributes + extra, 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") == "true", 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 derivation(_ node: XSDTree) -> XSDTree? {
|
|
XSDNode.firstChild(node, named: "restriction") ?? XSDNode.firstChild(node, named: "extension")
|
|
}
|
|
|
|
private static func simpleContentType(_ node: XSDTree, _ context: XSDContext) -> SimpleType {
|
|
guard let inner = derivation(node) else { return SimpleType(base: .string) }
|
|
let baseName = XSDNode.stripPrefix(XSDNode.attribute(inner, "base") ?? "string")
|
|
let base = BuiltinType(rawValue: baseName) ?? context.simpleTypes[baseName]?.base ?? .string
|
|
var facets = context.simpleTypes[baseName]?.facets ?? Facets()
|
|
XSDSimpleParser.applyFacets(inner, into: &facets)
|
|
return SimpleType(base: base, facets: facets)
|
|
}
|
|
|
|
// MARK: Attribute uses and groups
|
|
|
|
/// The `default` or `fixed` value constraint declared on an attribute or
|
|
/// element node, if any (`fixed` takes precedence).
|
|
static func valueConstraint(of node: XSDTree) -> ValueConstraint? {
|
|
if let fixed = XSDNode.attribute(node, "fixed") { return .fixed(fixed) }
|
|
if let value = XSDNode.attribute(node, "default") { return .default(value) }
|
|
return nil
|
|
}
|
|
|
|
// MARK: Model groups
|
|
|
|
static func modelGroup(in node: XSDTree, _ context: XSDContext) -> Particle? {
|
|
for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] {
|
|
if let group = XSDNode.firstChild(node, named: name) {
|
|
return groupParticle(group, compositor, context)
|
|
}
|
|
}
|
|
if let groupRef = XSDNode.firstChild(node, named: "group") {
|
|
return particle(groupRef, context)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func groupParticle(
|
|
_ node: XSDTree,
|
|
_ compositor: Compositor,
|
|
_ context: XSDContext,
|
|
) -> Particle {
|
|
var particles: [Particle] = []
|
|
for child in XSDNode.elementChildren(node) {
|
|
if let member = particle(child, context) { particles.append(member) }
|
|
}
|
|
let (minimum, maximum) = XSDNode.occurrence(node)
|
|
return Particle(
|
|
minOccurs: minimum,
|
|
maxOccurs: maximum,
|
|
term: .group(Group(compositor: compositor, particles: particles)),
|
|
)
|
|
}
|
|
|
|
private static func particle(_ node: XSDTree, _ context: XSDContext) -> Particle? {
|
|
let (minimum, maximum) = XSDNode.occurrence(node)
|
|
switch XSDNode.localName(node) {
|
|
case "element":
|
|
return elementParticle(node, minimum, maximum, context)
|
|
case "sequence":
|
|
return groupParticle(node, .sequence, context)
|
|
case "choice":
|
|
return groupParticle(node, .choice, context)
|
|
case "all":
|
|
return groupParticle(node, .all, context)
|
|
case "group":
|
|
return groupReferenceParticle(node, minimum, maximum, context)
|
|
case "any":
|
|
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(wildcard(node, context)))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func elementParticle(_ node: XSDTree, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle {
|
|
if let ref = XSDNode.attribute(node, "ref") {
|
|
return referenceParticle(ref, minimum, maximum, context)
|
|
}
|
|
let name = XSDNode.attribute(node, "name") ?? ""
|
|
return Particle(
|
|
minOccurs: minimum,
|
|
maxOccurs: maximum,
|
|
term: .element(
|
|
name: localElementName(name, XSDNode.attribute(node, "form"), context),
|
|
type: elementType(node, context),
|
|
typeName: elementTypeName(node),
|
|
),
|
|
)
|
|
}
|
|
|
|
private static func groupReferenceParticle(_ node: XSDTree, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle? {
|
|
guard let ref = XSDNode.attribute(node, "ref") else { return nil }
|
|
let name = XSDNode.stripPrefix(ref)
|
|
guard !context.visitingGroups.contains(name), let definition = context.groups[name],
|
|
let inner = modelGroup(in: definition, context.visiting(name))
|
|
else {
|
|
return nil
|
|
}
|
|
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureXML.Schema.XSDParser {
|
|
/// The particle for an `<xs:element ref="...">`. An abstract head may not appear
|
|
/// itself, only its substitution-group members; a concrete head appears alongside
|
|
/// them, the reference expanding to a choice over the head and every member.
|
|
static func referenceParticle(_ ref: String, _ minimum: Int, _ maximum: Int?, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Particle {
|
|
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
|
// The reference's QName is resolved by standard XML namespace rules (a bound
|
|
// prefix gives its namespace, so an imported element lands in the imported
|
|
// namespace; an unprefixed name the default namespace, or none), as the W3C
|
|
// XSTS suite expects: a bare `ref="foo"` with no default namespace is in no
|
|
// namespace, which `##other` admits; forcing it to the target over-rejects
|
|
// those and masks a dangling reference. Members are this schema's globals.
|
|
let refNamespace = PureXML.Schema.XSDNode.referenceNamespace(ref, context.namespaceBindings)
|
|
let head: [(String, String?)] = context.abstractElements.contains(name) ? [] : [(name, refNamespace)]
|
|
let alternatives = head + (context.substitutions[name] ?? []).map { ($0, context.targetNamespace) }
|
|
if alternatives.count == 1 {
|
|
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: elementReferenceTerm(alternatives[0].0, alternatives[0].1))
|
|
}
|
|
let members = alternatives.map { PureXML.Schema.Particle(term: elementReferenceTerm($0.0, $0.1)) }
|
|
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: .group(.init(compositor: .choice, particles: members)))
|
|
}
|
|
|
|
/// The term for a reference to a global element, in the resolved `namespace`.
|
|
static func elementReferenceTerm(_ name: String, _ namespace: String?) -> PureXML.Schema.Term {
|
|
// A reference's type is resolved indirectly through the element key, so its
|
|
// derivation identity is not a plain type name; NameAndTypeOK over a ref
|
|
// falls back to the structural check (typeName nil).
|
|
.element(
|
|
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: namespace),
|
|
type: .typeReference(elementKey(name)),
|
|
typeName: nil,
|
|
)
|
|
}
|
|
|
|
/// The qualified name of a local element declaration: in the target namespace
|
|
/// when `elementFormDefault` (or the element's own `form`) is qualified,
|
|
/// otherwise in no namespace.
|
|
static func localElementName(_ name: String, _ form: String?, _ context: PureXML.Schema.XSDContext) -> PureXML.Model.QualifiedName {
|
|
let qualified = form == "qualified" || (form == nil && context.elementFormQualified)
|
|
return PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? context.targetNamespace : nil)
|
|
}
|
|
}
|
|
|
|
extension PureXML.Schema.XSDParser {
|
|
/// Gathers `nillable` and `default`/`fixed` value constraints from every
|
|
/// element declaration at any depth, keyed by the element's name.
|
|
static func elementMetadata(_ containers: [XSDTree]) -> (Set<String>, [String: PureXML.Schema.ValueConstraint]) {
|
|
var nillable: Set<String> = []
|
|
var constraints: [String: PureXML.Schema.ValueConstraint] = [:]
|
|
for container in containers {
|
|
for element in descendants(container, named: "element") {
|
|
guard let name = PureXML.Schema.XSDNode.attribute(element, "name") else { continue }
|
|
if PureXML.Schema.XSDNode.attribute(element, "nillable") == "true" { nillable.insert(name) }
|
|
if let constraint = valueConstraint(of: element) { constraints[name] = constraint }
|
|
}
|
|
}
|
|
return (nillable, constraints)
|
|
}
|
|
|
|
/// Gathers identity constraints (`unique`, `key`, `keyref`) declared on any
|
|
/// element at any depth, keyed by the element's name.
|
|
static func identityConstraints(_ containers: [XSDTree]) -> [String: [PureXML.Schema.IdentityConstraint]] {
|
|
var map: [String: [PureXML.Schema.IdentityConstraint]] = [:]
|
|
for container in containers {
|
|
for element in descendants(container, named: "element") {
|
|
guard let name = PureXML.Schema.XSDNode.attribute(element, "name") else { continue }
|
|
let constraints = PureXML.Schema.XSDNode.elementChildren(element).compactMap(constraint)
|
|
if !constraints.isEmpty { map[name, default: []] += constraints }
|
|
}
|
|
}
|
|
return map
|
|
}
|
|
|
|
private static func constraint(_ node: XSDTree) -> PureXML.Schema.IdentityConstraint? {
|
|
let kind: PureXML.Schema.IdentityConstraintKind
|
|
switch PureXML.Schema.XSDNode.localName(node) {
|
|
case "unique": kind = .unique
|
|
case "key": kind = .key
|
|
case "keyref": kind = .keyref(refer: PureXML.Schema.XSDNode.stripPrefix(PureXML.Schema.XSDNode.attribute(node, "refer") ?? ""))
|
|
default: return nil
|
|
}
|
|
let selector = PureXML.Schema.XSDNode.firstChild(node, named: "selector").flatMap { PureXML.Schema.XSDNode.attribute($0, "xpath") } ?? ""
|
|
let fields = PureXML.Schema.XSDNode.children(node, named: "field").compactMap { PureXML.Schema.XSDNode.attribute($0, "xpath") }
|
|
return PureXML.Schema.IdentityConstraint(name: PureXML.Schema.XSDNode.attribute(node, "name") ?? "", kind: kind, selector: selector, fields: fields)
|
|
}
|
|
|
|
static func descendants(_ node: XSDTree, named name: String) -> [XSDTree] {
|
|
var result: [XSDTree] = []
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
if PureXML.Schema.XSDNode.localName(child) == name { result.append(child) }
|
|
result += descendants(child, named: name)
|
|
}
|
|
return result
|
|
}
|
|
}
|