PureXML/Sources/Schema/SchemaAllGroupPlacement.swift
Mihaela Mihaljevic ad110b101c fix(schema): reject an all-group referenced inside a compositor (cos-all-limited)
An xs:all group may only be the whole content model of a complex type. A direct
all nested in a sequence/choice was already rejected; this closes the case where
an all is reached through a <group ref> to a named group whose content is an all
and that reference appears inside a compositor.

The all-group pool is qualified by the defining schema's target namespace, and a
reference by its prefix binding, so an imported or redefined same-named group is
not confused with a local one (two over-rejections found by an adversarial
critic: a redefined all-group whose content is overridden, and an imported
all-group shadowing a local non-all group; both fixed and tested). A group
redefined to non-all content is exempt.

XSTS: invalid schemas accepted 481 to 480, no other bucket raised. Disclosed
under-rejections: a redefine that keeps the group an all, and cross-namespace
misplacement of an imported all-group.
2026-06-15 12:38:30 +02:00

74 lines
4.2 KiB
Swift

private typealias AllNode = PureXML.Schema.XSDNode
extension PureXML.Schema.XSDParser {
/// XSD 1.0 `cos-all-limited.1.2`: an `all` group may only be the whole content
/// model of a complex type. A direct `all` nested in a `sequence`/`choice` is
/// already rejected by the child-content table; the remaining case is an `all`
/// reached through a `<group ref>` to a named group whose content is an `all`,
/// where that reference appears inside a compositor (so the `all` is not the
/// whole content). The reference is followed only when it resolves to this
/// schema's own target namespace.
static func allGroupReferencePlacementErrors(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [String] {
// A redefined group's effective content is the redefinition's, not the
// original loaded definition, so a redefined group name is exempt: its
// original `all` content is overridden (and the suite treats a reference to
// it inside a compositor as valid).
var redefined: Set<String> = []
for container in containers where AllNode.localName(container) == "redefine" {
for group in AllNode.children(container, named: "group") {
if let name = AllNode.attribute(group, "name") { redefined.insert(name) }
}
}
var allGroups: Set<String> = []
for container in containers {
// A named group defines a component in the target namespace of the
// schema document that holds it. A reference is only ever followed when
// it resolves to this schema's own `target` (see `allGroupReference`),
// so pooling a same-named all-group from an *imported* namespace would
// wrongly flag a local non-all group referenced inside a compositor.
// Record only all-groups defined in `target`: a `schema` container's
// own namespace is its `targetNamespace`; a `redefine` container's
// redefinitions belong to the including schema (this `target`).
let containerNamespace = AllNode.localName(container) == "redefine"
? target
: AllNode.attribute(container, "targetNamespace")
guard containerNamespace == target else { continue }
for group in descendants(container, named: "group") {
guard let name = AllNode.attribute(group, "name"), !redefined.contains(name) else { continue }
if AllNode.elementChildren(group).contains(where: { AllNode.localName($0) == "all" }) {
allGroups.insert(name)
}
}
}
guard !allGroups.isEmpty else { return [] }
var errors: [String] = []
for container in containers {
collectAllGroupRefPlacement(container, allGroups, bindings, target, into: &errors)
}
return errors
}
private static func collectAllGroupRefPlacement(_ node: XSDTree, _ allGroups: Set<String>, _ bindings: [String: String], _ target: String?, into errors: inout [String]) {
let kind = AllNode.localName(node)
let insideCompositor = kind == "sequence" || kind == "choice" || kind == "all"
for child in AllNode.elementChildren(node) {
if insideCompositor, let referenced = allGroupReference(child, allGroups, bindings, target) {
errors.append("the all-group '\(referenced)' may not be referenced inside a '\(kind ?? "")'; an all group must be the whole content model")
}
collectAllGroupRefPlacement(child, allGroups, bindings, target, into: &errors)
}
}
/// The name of the all-group `child` references in this schema's target
/// namespace, or nil when `child` is not such a reference.
private static func allGroupReference(_ child: XSDTree, _ allGroups: Set<String>, _ bindings: [String: String], _ target: String?) -> String? {
guard AllNode.localName(child) == "group", let ref = AllNode.attribute(child, "ref"),
AllNode.referenceNamespace(ref, bindings) == target
else {
return nil
}
let name = AllNode.stripPrefix(ref)
return allGroups.contains(name) ? name : nil
}
}