diff --git a/CHANGELOG.md b/CHANGELOG.md index a4810dd..98fcc7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Schema/SchemaCompileValidation.swift b/Sources/Schema/SchemaCompileValidation.swift index 5b79851..e0d9b94 100644 --- a/Sources/Schema/SchemaCompileValidation.swift +++ b/Sources/Schema/SchemaCompileValidation.swift @@ -247,7 +247,7 @@ extension PureXML.Validation { AnyValidation(componentNamesUnique), AnyValidation(simpleTypeFinalControlsValid), AnyValidation(contentModelsDeterministic), AnyValidation(referencedSchemasResolveToSchemas), AnyValidation(redefinitionsDeriveFromThemselves), - AnyValidation(redefineGroupSelfReferencesAreUnit), + AnyValidation(redefineSelfReferencesAreWellFormed), ], reference: [ AnyValidation(typeDerivationAcyclic), diff --git a/Sources/Schema/SchemaCompositionValidations.swift b/Sources/Schema/SchemaCompositionValidations.swift index e92040f..f031f2c 100644 --- a/Sources/Schema/SchemaCompositionValidations.swift +++ b/Sources/Schema/SchemaCompositionValidations.swift @@ -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 { - 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 { + 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), ) } } diff --git a/Sources/Schema/SchemaRedefineSelfReference.swift b/Sources/Schema/SchemaRedefineSelfReference.swift new file mode 100644 index 0000000..a9534c0 --- /dev/null +++ b/Sources/Schema/SchemaRedefineSelfReference.swift @@ -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 + /// `` 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 + } +} diff --git a/Sources/Schema/XSDDerivation.swift b/Sources/Schema/XSDDerivation.swift index e439e52..a1b6a79 100644 --- a/Sources/Schema/XSDDerivation.swift +++ b/Sources/Schema/XSDDerivation.swift @@ -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 `` 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. diff --git a/Tests/SchemaIncludeTests.swift b/Tests/SchemaIncludeTests.swift index 73691c2..33c391b 100644 --- a/Tests/SchemaIncludeTests.swift +++ b/Tests/SchemaIncludeTests.swift @@ -94,6 +94,30 @@ struct SchemaIncludeTests { #expect((try? PureXML.Schema.Document(redefine(#""#), 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 = """ + + + + """ + let loader: (String) -> String? = { $0 == "a.xsd" ? original : nil } + func redefine(_ refs: String) -> String { + """ + + + \(refs) + + + """ + } + // Two self-references are invalid; one is valid. + #expect((try? PureXML.Schema.Document(redefine(#""#), schemaLoader: loader)) == nil) + #expect((try? PureXML.Schema.Document(redefine(#""#), 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 diff --git a/Tests/ValidationConfigurationTests.swift b/Tests/ValidationConfigurationTests.swift index 1421a89..48cb65c 100644 --- a/Tests/ValidationConfigurationTests.swift +++ b/Tests/ValidationConfigurationTests.swift @@ -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", diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 7a77652..a62d694 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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