fix(schema): enforce model-group redefine restriction (cos-group-restrict)
A non-self-referencing model group redefined inside xs:redefine must be a valid
restriction of the group it redefines (cos-group-restrict / src-redefine 6.1.2),
so its content model may not widen the original's language. The check reuses the
particle-restriction oracle used for complex-type restriction, resolves the
original group by namespaced identity with a {}name chameleon fallback, and runs
post-compile where element type compatibility is available.
Now rejected: adding required content to an empty base (schZ006), dropping a
required all member (schL1), reordering an all (schL5/L6, the Recurse rule is
order-preserving even for all), growing to a superset (schL8), and the
multi-document/chained redefines schN4/5/6/7 and schL10. To stay
false-positive-free the check skips a redefinition carrying a self-reference and
declines to judge a base with a maxOccurs=0 pointless particle (the contested
case W3C accepts, mgO013); genuine restrictions (dropping an optional member, an
unchanged redefinition) are accepted.
XSTS invalid-schemas-accepted 64 -> 51, no false positive (valid-schemas-rejected
and valid-instances-rejected held at 0; an over-rejection of mgO013 surfaced by
the gate was fixed before commit, and an adversarial critic confirmed FP-safety),
no other bucket moved. Disclosed under-rejections: self-referencing redefinitions
(schN10) and maxOccurs=0-base redefines.
This commit is contained in:
parent
1d17d2089f
commit
af5d7882b0
@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS invalid-schemas-accepted (#145, #161): a non-self-referencing model group redefined inside `xs:redefine` must be a valid RESTRICTION of the group it redefines (`cos-group-restrict` / `src-redefine` 6.1.2), so its content model may not widen the original's language. The check reuses the same particle-restriction oracle as complex-type restriction, resolving the original group by namespaced identity (with a chameleon `{}name` fallback for a no-targetNamespace redefined schema). Cases now rejected include adding required content to an empty base (schZ006), dropping a required `all` member so documents without it become valid (schL1), reordering an `all` (schL5/L6, since the spec's Recurse rule is order-preserving even for `all`), and growing a group to a superset (schL8), plus the multi-document and chained redefines schN4/5/6/7 and schL10. To stay false-positive-free the check skips a redefinition carrying a self-reference (governed by the self-reference well-formedness rule and not re-parseable here) and declines to judge a base containing a `maxOccurs=0` "pointless particle" (the contested normalization case where W3C accepts a redefinition re-enabling that member, mgO013); a genuine restriction (dropping an optional member, an unchanged redefinition) is accepted. invalid-schemas-accepted 64 → 51, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; an over-rejection of mgO013 surfaced by the gate was fixed before commit), no other bucket moved. Disclosed under-rejections: self-referencing redefinitions (schN10) and `maxOccurs=0`-base redefines.
|
||||
|
||||
- XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group restricts the original, so it may neither eliminate a required attribute the original declares, nor re-introduce a prohibited one as usable, nor relax a fixed value the original sets (src-redefine / cos-ct-restricts). attgZ002 (dropping the required `att1`), attgZ003 (re-adding the prohibited `att1` as optional), and schM10 (changing a `fixed` attribute to a `default` one) are now rejected. Re-declaring a prohibited attribute AS prohibited keeps it prohibited and stays valid, the drop-required check is skipped when the redefinition uses a nested attributeGroup reference (which could re-introduce the attribute), and the fixed-value check fires only when the redefinition declares the matching attribute as non-prohibited with a differing fixed value (keeping the same fixed value, prohibiting it, or omitting it is left alone), so no valid redefinition is rejected. invalid-schemas-accepted 67 → 64, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.
|
||||
|
||||
- A complex type whose `simpleContent` derives from a list or union simple type now keeps that variety, so its element content is validated as a list or union rather than collapsing to an atomic value space. Previously `simpleContentType` dropped the base's variety, so an invalid item in a list-typed simpleContent (e.g. a non-int in a list-of-int) was accepted. A valid list/union value is unaffected. (Under-rejection fix surfaced by the libxml2 differential; the W3C suite does not exercise it, so the conformance counts are unchanged.)
|
||||
|
||||
@ -267,7 +267,7 @@ extension PureXML.Validation {
|
||||
AnyValidation(userTypeValueConstraintsValid),
|
||||
AnyValidation(extensionAllGroupsValid),
|
||||
AnyValidation(anonymousRestrictionsValid),
|
||||
AnyValidation(complexExtensionBaseValid),
|
||||
AnyValidation(complexExtensionBaseValid), AnyValidation(redefinedGroupsRestrictBase),
|
||||
AnyValidation(attributeRestrictionsFaithful),
|
||||
AnyValidation(simpleTypeBasesAreSimple),
|
||||
AnyValidation(simpleTypeVarietiesValid),
|
||||
|
||||
@ -45,6 +45,24 @@ extension PureXML.Validation.SchemaCompile {
|
||||
}
|
||||
}
|
||||
|
||||
/// cos-group-restrict: a model group redefined through `xs:redefine` restricts the
|
||||
/// original, so its content model must accept a subset of the original's. Reuses
|
||||
/// the particle-restriction oracle; runs after named types resolve so element type
|
||||
/// compatibility is available.
|
||||
static var redefinedGroupsRestrictBase: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("A redefined model group restricts the group it redefines") { document in
|
||||
guard let namedTypes = document.namedTypes, let derivation = document.derivation else { return [] }
|
||||
return PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.redefineGroupRestrictionErrors(
|
||||
document.containers,
|
||||
context: document.context,
|
||||
types: namedTypes,
|
||||
derivation: derivation.typeDerivation,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Facet definition validity and pattern syntax validity (XSD Part 2 4.3): a
|
||||
/// constraining facet must be applicable to and valid for its base type, and a
|
||||
/// `pattern` must compile. These are gathered during simple-type compilation
|
||||
|
||||
@ -128,6 +128,77 @@ extension PureXML.Schema.XSDParser {
|
||||
return RedefineNode.attribute(redefined, "fixed") != fixed
|
||||
}
|
||||
|
||||
/// cos-group-restrict: a model group redefined inside `xs:redefine` is a
|
||||
/// RESTRICTION of the group it redefines, so its content model must accept a
|
||||
/// subset of the original's. The check reuses the same particle-restriction
|
||||
/// oracle as complex-type restriction (`ParticleRestriction.violation`). To stay
|
||||
/// false-positive-free it runs only when the original group resolves by namespaced
|
||||
/// identity and when the redefinition has NO self-reference (a `<group ref>` to its
|
||||
/// own name expands to the original and is governed by the self-reference
|
||||
/// well-formedness rule instead, and cannot be re-parsed reliably here). An
|
||||
/// order-independent `all` reordering accepts the same language, so the oracle
|
||||
/// correctly does not flag it; only a genuine content widening is reported.
|
||||
static func redefineGroupRestrictionErrors(
|
||||
_ containers: [XSDTree],
|
||||
context: PureXML.Schema.XSDContext,
|
||||
types: [String: PureXML.Schema.ElementType],
|
||||
derivation: [String: PureXML.Schema.TypeDerivation],
|
||||
) -> [String] {
|
||||
var base: [String: XSDTree] = [:]
|
||||
for container in containers where RedefineNode.localName(container) != "redefine" {
|
||||
for group in RedefineNode.children(container, named: "group") {
|
||||
if let name = RedefineNode.attribute(group, "name") { base[namespacedKey(name, owner: group)] = group }
|
||||
}
|
||||
}
|
||||
var errors: [String] = []
|
||||
for container in containers where RedefineNode.localName(container) == "redefine" {
|
||||
for redefinition in RedefineNode.children(container, named: "group") {
|
||||
guard let name = RedefineNode.attribute(redefinition, "name"),
|
||||
!hasGroupSelfReference(redefinition, name: name),
|
||||
// The redefined schema's components live in the redefining schema's
|
||||
// own namespace, or in none when it is a chameleon (no
|
||||
// targetNamespace) absorbed on redefine; try both keys. A base with
|
||||
// a DIFFERENT explicit namespace keys under neither, so no
|
||||
// cross-namespace component is mistaken for the original.
|
||||
let original = base[namespacedKey(name, owner: redefinition)] ?? base["{}\(name)"],
|
||||
let redefined = modelGroup(in: redefinition, context),
|
||||
let baseline = modelGroup(in: original, context),
|
||||
// A `maxOccurs=0` ("pointless") member in the original puts the
|
||||
// redefine into contested cos-group-restrict territory (the spec's
|
||||
// normalization removes it, yet XSTS accepts a redefinition that
|
||||
// re-enables that very member, e.g. mgO013). Decline to judge such
|
||||
// a base rather than risk over-rejecting a valid redefine; this
|
||||
// only withholds a rejection, so it cannot introduce a false positive.
|
||||
!hasPointlessParticle(baseline)
|
||||
else { continue }
|
||||
if let reason = PureXML.Schema.ParticleRestriction.violation(
|
||||
restricted: .elementOnly(redefined), base: .elementOnly(baseline), types: types, derivation: derivation,
|
||||
) {
|
||||
errors.append("a redefined model group '\(name)' must restrict the group it redefines: \(reason)")
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// Whether `particle` or any of its descendants can never occur (`maxOccurs=0`), a
|
||||
/// "pointless particle" whose normalization in the redefine-restriction check is
|
||||
/// contested.
|
||||
private static func hasPointlessParticle(_ particle: PureXML.Schema.Particle) -> Bool {
|
||||
if particle.maxOccurs == 0 { return true }
|
||||
if case let .group(group) = particle.term { return group.particles.contains(where: hasPointlessParticle) }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether a redefinition group contains a self-reference (`<group ref>` to its own
|
||||
/// name within its bounded model), which expands to the original and is handled by
|
||||
/// the self-reference well-formedness rule rather than re-parsed here.
|
||||
private static func hasGroupSelfReference(_ redefinition: XSDTree, name: String) -> Bool {
|
||||
boundedSelfReferenceNodes(redefinition, "group").contains { ref in
|
||||
RedefineNode.attribute(ref, "ref").map(RedefineNode.stripPrefix) == name
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
77
Tests/SchemaGroupRedefineRestrictionTests.swift
Normal file
77
Tests/SchemaGroupRedefineRestrictionTests.swift
Normal file
@ -0,0 +1,77 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// A non-self-referencing model group redefined inside `xs:redefine` must be a valid
|
||||
/// restriction of the group it redefines (XSD 1.0 cos-group-restrict / src-redefine
|
||||
/// 6.1.2). Adding content the original cannot produce, or dropping a required member
|
||||
/// so the language widens, is rejected; a genuine restriction is accepted. The
|
||||
/// order-preserving Recurse rule applies even to `xs:all`, so reordering an `all` is
|
||||
/// rejected (W3C schL5), while a base with a `maxOccurs=0` member is left unjudged to
|
||||
/// avoid the contested pointless-particle case (W3C mgO013).
|
||||
@Suite("redefined model group restriction")
|
||||
struct SchemaGroupRedefineRestrictionTests {
|
||||
private func redefine(group body: String, base: String) throws -> PureXML.Schema.Document {
|
||||
let main = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:redefine schemaLocation='b.xsd'><xs:group name='g'>\(body)</xs:group></xs:redefine>"
|
||||
+ "<xs:complexType name='t'><xs:group ref='g'/></xs:complexType>"
|
||||
+ "<xs:element name='e' type='t'/></xs:schema>"
|
||||
let baseDoc = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:group name='g'>\(base)</xs:group>"
|
||||
+ "<xs:complexType name='b'><xs:group ref='g'/></xs:complexType></xs:schema>"
|
||||
return try PureXML.Schema.Document(main, schemaLoader: { _ in baseDoc })
|
||||
}
|
||||
|
||||
@Test("adding required content to an empty base group is rejected")
|
||||
func test_addRequiredToEmptyRejected() throws {
|
||||
#expect(throws: (any Error).self) {
|
||||
try redefine(
|
||||
group: "<xs:sequence><xs:element name='a' type='xs:string'/></xs:sequence>",
|
||||
base: "<xs:sequence/>",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("dropping a required member from an all group is rejected")
|
||||
func test_dropRequiredFromAllRejected() throws {
|
||||
#expect(throws: (any Error).self) {
|
||||
try redefine(
|
||||
group: "<xs:all><xs:element name='a' type='xs:string'/></xs:all>",
|
||||
base: "<xs:all><xs:element name='a' type='xs:string'/><xs:element name='b' type='xs:string'/></xs:all>",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("reordering an all group is rejected (order-preserving Recurse)")
|
||||
func test_reorderAllRejected() throws {
|
||||
#expect(throws: (any Error).self) {
|
||||
try redefine(
|
||||
group: "<xs:all><xs:element name='b' type='xs:string'/><xs:element name='a' type='xs:string'/></xs:all>",
|
||||
base: "<xs:all><xs:element name='a' type='xs:string'/><xs:element name='b' type='xs:string'/></xs:all>",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("dropping an optional member is a valid restriction")
|
||||
func test_dropOptionalAccepted() throws {
|
||||
_ = try redefine(
|
||||
group: "<xs:sequence><xs:element name='a' type='xs:string'/></xs:sequence>",
|
||||
base: "<xs:sequence><xs:element name='a' type='xs:string'/><xs:element name='b' type='xs:string' minOccurs='0'/></xs:sequence>",
|
||||
)
|
||||
}
|
||||
|
||||
@Test("re-enabling a maxOccurs=0 base member is left unjudged and accepted")
|
||||
func test_pointlessParticleBaseAccepted() throws {
|
||||
_ = try redefine(
|
||||
group: "<xs:all><xs:element name='a' type='xs:string'/></xs:all>",
|
||||
base: "<xs:all><xs:element name='a' type='xs:string' minOccurs='0' maxOccurs='0'/></xs:all>",
|
||||
)
|
||||
}
|
||||
|
||||
@Test("an unchanged group redefinition is a valid restriction")
|
||||
func test_identityAccepted() throws {
|
||||
_ = try redefine(
|
||||
group: "<xs:sequence><xs:element name='a' type='xs:string'/></xs:sequence>",
|
||||
base: "<xs:sequence><xs:element name='a' type='xs:string'/></xs:sequence>",
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -107,6 +107,7 @@ struct ValidationConfigurationTests {
|
||||
"Complex types extending xs:all groups satisfy XSD placement rules",
|
||||
"Anonymous complex-type restrictions accept a subset of their base's",
|
||||
"A content-model derivation is consistent with its base type",
|
||||
"A redefined model group restricts the group it redefines",
|
||||
"Attribute restrictions are faithful to their bases",
|
||||
"Simple types do not derive from complex types",
|
||||
"Simple-type varieties are declared consistently",
|
||||
|
||||
@ -148,7 +148,7 @@ struct XSTSSuiteTests {
|
||||
/// and the cross-document composition family (src-resolve, src-import.3,
|
||||
/// src-redefine.5/6.1/7.2.1, redefine cross-namespace, required schemaLocation).
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 64
|
||||
private let knownSchemaInvalidAccepted = 51
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 31), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// declared type (complex, then atomic and list/union, resolved through the element-ref chain);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user