PureXML/Sources/Schema/SchemaContentOrder.swift
Mihaela Mihaljevic b0479fd329 fix(schema): require a content derivation in simpleContent/complexContent (#145)
A complexContent or simpleContent's content model requires exactly one
restriction or extension. An empty <complexContent/> declares no derivation
and is now rejected as a structural error.

XSTS invalid-schemas-accepted 200 to 197, no other bucket moved.
2026-06-17 14:58:58 +02:00

153 lines
8.4 KiB
Swift

extension PureXML.Schema.XSDParser {
/// The finding, if any, for a `simpleType`: its content is exactly one of
/// `restriction`/`list`/`union` (XSD 1.0 Datatypes), so neither an empty
/// `simpleType` nor one combining two derivations (two unions, a union and a
/// list, ...) is valid. The `allowedChildren` membership check admits each name
/// but not the cardinality.
static func simpleTypeContentErrors(_ children: [String]) -> [String] {
let derivations = children.count { $0 == "restriction" || $0 == "list" || $0 == "union" }
return derivations == 1 ? [] : ["a simpleType must contain exactly one of restriction, list, or union"]
}
/// Content-model order and cardinality for a `complexContent` derivation, the
/// ordering complement to the set-membership `allowedChildren` check (which
/// admits the right child names but not their sequence or count).
///
/// A `restriction`/`extension` inside `complexContent` has the model
/// `(annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?)`
/// (XSD 1.0 Structures): an optional model group, then any number of
/// `attribute`/`attributeGroup`, then an optional `anyAttribute`. A second model
/// group, a model group after an attribute, an attribute after `anyAttribute`, or
/// a second `anyAttribute` (the ctG/ctH families) was previously accepted.
/// Only the `complexContent` context is checked, where a model group is part of
/// the model; a `simpleContent` derivation (facets, no model group) has a
/// different model and is left to its own rules.
/// The number of `restriction`/`extension` children of a `simpleContent` or
/// `complexContent`. Its content model requires exactly one, so zero (an empty
/// `<complexContent/>`) or more than one is a structural error.
private static func derivationCount(_ content: XSDTree) -> Int {
PureXML.Schema.XSDNode.elementChildren(content).count { child in
guard child.name?.namespaceURI == xsdNamespace, let local = PureXML.Schema.XSDNode.localName(child) else { return false }
return local == "restriction" || local == "extension"
}
}
static func complexContentOrderErrors(_ complexContent: XSDTree) -> [String] {
var errors: [String] = []
if derivationCount(complexContent) != 1 {
errors.append("a complexContent must contain exactly one 'restriction' or 'extension'")
}
for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) {
guard let local = PureXML.Schema.XSDNode.localName(derivation),
local == "restriction" || local == "extension"
else { continue }
let names = PureXML.Schema.XSDNode.elementChildren(derivation)
.filter { $0.name?.namespaceURI == xsdNamespace }
.compactMap(PureXML.Schema.XSDNode.localName)
.filter { $0 != "annotation" }
if let reason = slotOrderViolation(names, slots: complexDerivationSlots) {
errors.append("the content of a complexContent '\(local)' must be a model group, then attributes, then anyAttribute: \(reason)")
}
}
return errors
}
/// Content-model order for a complexType's shorthand form (no `simpleContent`/
/// `complexContent`), which shares the derivation's slot model: an optional model
/// group, then `attribute`/`attributeGroup`, then an optional `anyAttribute`.
/// `complexTypeContentErrors` already checks cardinality and the exclusivity of a
/// content spec with model groups/attributes; this adds the ordering (a model
/// group after an attribute, an attribute after `anyAttribute`, a second
/// `anyAttribute`). `annotation` and the content specs are not part of the slots.
static func complexTypeOrderErrors(_ complexType: XSDTree) -> [String] {
let names = PureXML.Schema.XSDNode.elementChildren(complexType)
.filter { $0.name?.namespaceURI == xsdNamespace }
.compactMap(PureXML.Schema.XSDNode.localName)
.filter { $0 != "annotation" && $0 != "simpleContent" && $0 != "complexContent" }
guard let reason = slotOrderViolation(names, slots: complexDerivationSlots) else { return [] }
return ["the content of a complexType must be a model group, then attributes, then anyAttribute: \(reason)"]
}
/// Content-model order for a `simpleContent` derivation (XSD 1.0 Structures),
/// whose model has NO model group: a `restriction` is
/// `(annotation?, (simpleType?, facet*), (attribute | attributeGroup)*, anyAttribute?)`
/// and an `extension` is `(annotation?, (attribute | attributeGroup)*, anyAttribute?)`.
/// So a model group anywhere, a facet or `simpleType` in an extension, a facet
/// before its `simpleType`, an attribute before a facet, an attribute after
/// `anyAttribute`, or a second `anyAttribute` was accepted by the membership
/// table but is invalid (the ctD family).
static func simpleContentOrderErrors(_ simpleContent: XSDTree) -> [String] {
var errors: [String] = []
if derivationCount(simpleContent) != 1 {
errors.append("a simpleContent must contain exactly one 'restriction' or 'extension'")
}
for derivation in PureXML.Schema.XSDNode.elementChildren(simpleContent) {
guard let local = PureXML.Schema.XSDNode.localName(derivation) else { continue }
let slots: [(members: Set<String>, max: Int?)]
switch local {
case "restriction": slots = simpleContentRestrictionSlots
case "extension": slots = simpleContentExtensionSlots
default: continue
}
let names = PureXML.Schema.XSDNode.elementChildren(derivation)
.filter { $0.name?.namespaceURI == xsdNamespace }
.compactMap(PureXML.Schema.XSDNode.localName)
.filter { $0 != "annotation" }
if let reason = slotOrderViolation(names, slots: slots) {
errors.append("the content of a simpleContent '\(local)' is malformed: \(reason)")
}
}
return errors
}
/// The ordered slots of a `complexContent` derivation: each lists its admitted
/// names and how many children may fill it (nil is unbounded).
private static let complexDerivationSlots: [(members: Set<String>, max: Int?)] = [
(["group", "all", "choice", "sequence"], 1),
(["attribute", "attributeGroup"], nil),
(["anyAttribute"], 1),
]
private static let constrainingFacets: Set<String> = [
"minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits",
"fractionDigits", "length", "minLength", "maxLength", "enumeration", "whiteSpace", "pattern",
]
/// The ordered slots of a `simpleContent` `restriction`: an optional `simpleType`,
/// then facets, then attributes, then an optional `anyAttribute`.
private static let simpleContentRestrictionSlots: [(members: Set<String>, max: Int?)] = [
(["simpleType"], 1),
(constrainingFacets, nil),
(["attribute", "attributeGroup"], nil),
(["anyAttribute"], 1),
]
/// The ordered slots of a `simpleContent` `extension`: attributes, then an
/// optional `anyAttribute` (no facets, `simpleType`, or model group).
private static let simpleContentExtensionSlots: [(members: Set<String>, max: Int?)] = [
(["attribute", "attributeGroup"], nil),
(["anyAttribute"], 1),
]
/// The first child that does not fit the ordered slot model, or nil. Each child
/// must fill the current slot or one after it (order is forward-only), and a slot
/// with a finite `max` may not be overfilled.
private static func slotOrderViolation(_ children: [String], slots: [(members: Set<String>, max: Int?)]) -> String? {
var slotIndex = 0
var counts = [Int](repeating: 0, count: slots.count)
for child in children {
var index = slotIndex
while index < slots.count, !slots[index].members.contains(child) {
index += 1
}
guard index < slots.count else { return "'\(child)' is out of order" }
counts[index] += 1
if let maximum = slots[index].max, counts[index] > maximum {
return "'\(child)' appears too many times"
}
slotIndex = index
}
return nil
}
}