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:
parent
6379e5b25a
commit
0f90b3a4d3
@ -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.
|
||||
|
||||
|
||||
@ -247,7 +247,7 @@ extension PureXML.Validation {
|
||||
AnyValidation(componentNamesUnique), AnyValidation(simpleTypeFinalControlsValid),
|
||||
AnyValidation(contentModelsDeterministic),
|
||||
AnyValidation(referencedSchemasResolveToSchemas), AnyValidation(redefinitionsDeriveFromThemselves),
|
||||
AnyValidation(redefineGroupSelfReferencesAreUnit),
|
||||
AnyValidation(redefineSelfReferencesAreWellFormed),
|
||||
],
|
||||
reference: [
|
||||
AnyValidation(typeDerivationAcyclic),
|
||||
|
||||
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
68
Sources/Schema/SchemaRedefineSelfReference.swift
Normal file
68
Sources/Schema/SchemaRedefineSelfReference.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user