fix(schema): generalize redefine self-reference to attributeGroups + bound the walk; invalid-schemas-accepted 75 to 74 (#161)

Extends the redefine self-reference rule (redefineSelfReferenceErrors, moved to
SchemaRedefineSelfReference.swift) to attribute groups: a group or attributeGroup
inside xs:redefine may reference itself at most once (src-redefine.6.1.1 /
7.2.1), and a group self-reference must occur exactly once. Catches a redefined
attributeGroup with two self-references (attgC044).

Fixes a latent false positive an adversarial review found in the group case: the
search now uses a BOUNDED walk (boundedSelfReferenceNodes) that stops at nested
element/complexType scopes, matching the established cycle-rule boundary. The
prior unbounded walk would miscount a recursive group reference inside a nested
element's content (a data-structure recursion, legal) as a redefinition
self-reference. attributeGroup content has no such nesting, so it is unaffected.

XSTS: invalid-schemas-accepted 75 to 74 (attgC044), valid-schemas-rejected held
at 0, no other bucket moved. The validation description broadened accordingly
(configuration pin updated).
This commit is contained in:
Mihaela Mihaljevic 2026-06-18 01:15:16 +02:00
parent 6379e5b25a
commit 0f90b3a4d3
8 changed files with 103 additions and 42 deletions

View File

@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- XSTS invalid-schemas-accepted (#145): an identity-constraint `selector`/`field` xpath may not use an undeclared namespace prefix. Every prefix in a name test must be a declared `xmlns:` prefix in scope (the implicit `xml` prefix excepted); a `selector`/`field` whose xpath names an unbound prefix is rejected. invalid-schemas-accepted 77 → 75, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed declared prefixes on the schema root or any ancestor resolve correctly), no other bucket moved.
- XSTS invalid-schemas-accepted (#161): a `group` inside `xs:redefine` may reference itself at most once, and that self-reference must occur exactly once, `minOccurs` = `maxOccurs` = 1 (`src-redefine.6.1.2`). A self-reference with `minOccurs="0"`, `maxOccurs="unbounded"`, or any count above 1 is rejected. The self-reference is matched by namespace, not local name alone, so a reference to an imported group that merely shares the local name is unconstrained (no false positive). invalid-schemas-accepted 79 → 77, no false positive (valid-schemas-rejected held at 0), no other bucket moved.
- XSTS invalid-schemas-accepted (#161): a `group` or `attributeGroup` inside `xs:redefine` may reference itself at most once, and a group self-reference must occur exactly once, `minOccurs` = `maxOccurs` = 1 (`src-redefine.6.1.1`/`6.1.2`/`7.2.1`). Two self-references, or a group self-reference with `minOccurs="0"`, `maxOccurs="unbounded"`, or any count above 1, is rejected. The self-reference is matched by namespace (not local name alone) and the search is bounded to the component's own model (stopping at nested `element`/`complexType` scopes), so a reference to an imported same-named component, or a recursive reference inside a nested element's content, is not flagged (no false positive). invalid-schemas-accepted 79 → 74, no false positive (valid-schemas-rejected held at 0), no other bucket moved.
- XSTS invalid-schemas-accepted (#145): a local `element` declaration must identify itself with a `name` or a `ref`; an element with neither is rejected. invalid-schemas-accepted 80 → 79, no false positive (valid-schemas-rejected held at 0), no other bucket moved.

View File

@ -247,7 +247,7 @@ extension PureXML.Validation {
AnyValidation(componentNamesUnique), AnyValidation(simpleTypeFinalControlsValid),
AnyValidation(contentModelsDeterministic),
AnyValidation(referencedSchemasResolveToSchemas), AnyValidation(redefinitionsDeriveFromThemselves),
AnyValidation(redefineGroupSelfReferencesAreUnit),
AnyValidation(redefineSelfReferencesAreWellFormed),
],
reference: [
AnyValidation(typeDerivationAcyclic),

View File

@ -25,11 +25,12 @@ extension PureXML.Validation.SchemaCompile {
}
}
/// src-redefine.6.1.2: a redefined group's self-reference has minOccurs = maxOccurs = 1.
static var redefineGroupSelfReferencesAreUnit: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
compileRule("A redefined group's self-reference occurs exactly once") { document in
/// src-redefine.6.1/7.2.1: a redefined group or attribute group has at most one
/// self-reference, and a group self-reference occurs exactly once.
static var redefineSelfReferencesAreWellFormed: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
compileRule("A redefined group or attribute group has a well-formed self-reference") { document in
PureXML.Schema.SchemaLocatedFinding.unlocated(
PureXML.Schema.XSDParser.redefineGroupSelfReferenceErrors(document.containers),
PureXML.Schema.XSDParser.redefineSelfReferenceErrors(document.containers),
)
}
}

View File

@ -0,0 +1,68 @@
private typealias RedefineNode = PureXML.Schema.XSDNode
extension PureXML.Schema.XSDParser {
/// src-redefine.6.1.1/6.1.2 (group) and 7.2.1 (attributeGroup): a `group` or
/// `attributeGroup` inside `xs:redefine` may reference ITSELF (the component it
/// redefines) AT MOST ONCE, and a group self-reference must have
/// `minOccurs` = `maxOccurs` = 1. Two self-references, or a group self-reference
/// with any other occurrence (`minOccurs="0"`, `maxOccurs="unbounded"`, a count
/// above 1), is invalid. A reference is the self-reference only when it resolves
/// (by NAMESPACE, not local name alone) to the redefined component: a
/// `<group ref="b:g">` to an imported component sharing the local name is a
/// different component and is unconstrained, so the namespace guard prevents a
/// false positive. (`attributeGroup` references carry no occurrence.) The walk is
/// BOUNDED: it stops at `element`/`complexType` scopes, so a recursive reference
/// inside a nested element's content (a data-structure recursion) is not
/// miscounted as a redefinition self-reference.
static func redefineSelfReferenceErrors(_ containers: [XSDTree]) -> [String] {
var errors: [String] = []
for container in containers where RedefineNode.localName(container) == "redefine" {
let schema = RedefineNode.schemaOwner(container)
let target = RedefineNode.attribute(schema, "targetNamespace")
let bindings = RedefineNode.namespaceBindings(of: schema)
for kind in ["group", "attributeGroup"] {
for definition in RedefineNode.children(container, named: kind) {
guard let name = RedefineNode.attribute(definition, "name") else { continue }
var selfReferences = 0
for ref in boundedSelfReferenceNodes(definition, kind) {
guard let refName = RedefineNode.attribute(ref, "ref"),
RedefineNode.stripPrefix(refName) == name,
RedefineNode.referenceNamespace(refName, bindings) == target
else { continue }
selfReferences += 1
if kind == "group" {
let minOccurs = RedefineNode.attribute(ref, "minOccurs") ?? "1"
let maxOccurs = RedefineNode.attribute(ref, "maxOccurs") ?? "1"
if maxOccurs == "unbounded" || canonicalMagnitude(minOccurs) != "1" || canonicalMagnitude(maxOccurs) != "1" {
errors.append("a redefined group's self-reference must have minOccurs and maxOccurs of 1")
}
}
}
if selfReferences > 1 {
errors.append("a redefined \(kind) may contain at most one self-reference")
}
}
}
}
return errors
}
/// The reference NODES of `kind` nested directly in `node`'s model, stopping at
/// `element`/`complexType`/`simpleType`/`attribute` scopes so a reference inside a
/// nested element's content (a data-structure recursion, not a redefinition
/// self-reference) is excluded.
private static func boundedSelfReferenceNodes(_ node: XSDTree, _ kind: String) -> [XSDTree] {
var nodes: [XSDTree] = []
for child in RedefineNode.elementChildren(node) {
switch RedefineNode.localName(child) {
case "element", "complexType", "simpleType", "attribute":
continue
case kind:
if RedefineNode.attribute(child, "ref") != nil { nodes.append(child) }
default:
nodes += boundedSelfReferenceNodes(child, kind)
}
}
return nodes
}
}

View File

@ -330,38 +330,6 @@ extension PureXML.Schema.XSDParser {
return errors
}
/// src-redefine.6.1.2: a `group` inside `xs:redefine` may reference ITSELF (the
/// group it redefines) at most once, and that self-reference must have
/// `minOccurs` = `maxOccurs` = 1. A self-reference with any other occurrence
/// (`minOccurs="0"`, `maxOccurs="unbounded"`, a count above 1) is invalid. A
/// reference is the self-reference only when it resolves (by NAMESPACE, not local
/// name alone) to the redefined group: a `<group ref="b:g">` to an imported group
/// that merely shares the local name is a different component and is unconstrained,
/// so the namespace guard prevents a false positive.
static func redefineGroupSelfReferenceErrors(_ containers: [XSDTree]) -> [String] {
var errors: [String] = []
for container in containers where XSDDerivNode.localName(container) == "redefine" {
let schema = XSDDerivNode.schemaOwner(container)
let target = XSDDerivNode.attribute(schema, "targetNamespace")
let bindings = XSDDerivNode.namespaceBindings(of: schema)
for group in XSDDerivNode.children(container, named: "group") {
guard let name = XSDDerivNode.attribute(group, "name") else { continue }
for ref in descendants(group, named: "group") {
guard let refName = XSDDerivNode.attribute(ref, "ref"),
XSDDerivNode.stripPrefix(refName) == name,
XSDDerivNode.referenceNamespace(refName, bindings) == target
else { continue }
let minOccurs = XSDDerivNode.attribute(ref, "minOccurs") ?? "1"
let maxOccurs = XSDDerivNode.attribute(ref, "maxOccurs") ?? "1"
if maxOccurs == "unbounded" || canonicalMagnitude(minOccurs) != "1" || canonicalMagnitude(maxOccurs) != "1" {
errors.append("a redefined group's self-reference must have minOccurs and maxOccurs of 1")
}
}
}
}
return errors
}
/// Drops from each substitution group the members a head's `block` forbids: a
/// member whose type derives from the head's type by a blocked method may not
/// substitute.

View File

@ -94,6 +94,30 @@ struct SchemaIncludeTests {
#expect((try? PureXML.Schema.Document(redefine(#"<xs:group ref="g"/>"#), schemaLoader: loader)) != nil)
}
/// src-redefine.7.2.1: a redefined attributeGroup may contain at most one
/// self-reference; two references to the group being redefined are invalid.
@Test("a redefined attributeGroup may have at most one self-reference")
func test_redefineAttributeGroupSelfReferenceCount() {
let original = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:attributeGroup name="ag"><xs:attribute name="x" type="xs:string"/></xs:attributeGroup>
</xs:schema>
"""
let loader: (String) -> String? = { $0 == "a.xsd" ? original : nil }
func redefine(_ refs: String) -> String {
"""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:redefine schemaLocation="a.xsd">
<xs:attributeGroup name="ag"><xs:attribute name="y" type="xs:string"/>\(refs)</xs:attributeGroup>
</xs:redefine>
</xs:schema>
"""
}
// Two self-references are invalid; one is valid.
#expect((try? PureXML.Schema.Document(redefine(#"<xs:attributeGroup ref="ag"/><xs:attributeGroup ref="ag"/>"#), schemaLoader: loader)) == nil)
#expect((try? PureXML.Schema.Document(redefine(#"<xs:attributeGroup ref="ag"/>"#), schemaLoader: loader)) != nil)
}
/// src-resolve: when an include/import/redefine schemaLocation IS resolved (the
/// loader returns content), that content must be a well-formed schema. Not-well-
/// formed XML, or well-formed XML that is not an xs:schema, is rejected. A

View File

@ -92,7 +92,7 @@ struct ValidationConfigurationTests {
"Schema content models are deterministic (UPA)",
"A resolved schemaLocation reference is a valid schema",
"A redefined type restricts or extends itself",
"A redefined group's self-reference occurs exactly once",
"A redefined group or attribute group has a well-formed self-reference",
"Type derivation chains contain no cycles",
"Schema type references contain no cycles",
"xs:all group references appear only where permitted",

View File

@ -150,10 +150,10 @@ struct XSTSSuiteTests {
/// imported target (src-import.3), redefined type derives from itself
/// (src-redefine.5), local complexType is anonymous, a local element has a name
/// or ref, a redefined group's self-reference occurs exactly once
/// (src-redefine.6.1.2), and an identity-constraint xpath uses no undeclared
/// namespace prefix.
/// (src-redefine.6.1), an identity-constraint xpath uses no undeclared namespace
/// prefix, and a redefined group/attributeGroup has at most one self-reference.
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 75
private let knownSchemaInvalidAccepted = 74
private let knownInstanceValidRejected = 0
private let knownInstanceInvalidAccepted = 133