fix(schema): reject a constraining facet on anySimpleType-based simpleContent
A constraining facet may not be applied where a simpleContent restriction's base chain resolves to xs:anySimpleType, the ur simple type, which carries no constraining facets (XSD Part 2 3.2.1); stZ010 (a complex type extending xs:anySimpleType, restricted by another that adds minLength) is now rejected. The base chain is followed through local complex types' own simpleContent bases; it stops at any other built-in, a simple type, or an external/unresolved base, and a restriction supplying its own nested simpleType base is skipped (the facet applies to that), so a facet on a string-, int-, or other real-typed simpleContent stays accepted. XSTS invalid-schemas-accepted 49 -> 48, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; an adversarial critic probed 12 valid schemas across every resolver branch and confirmed FP-safety), no other bucket moved.
This commit is contained in:
parent
afad5f822c
commit
f8ba6fef5e
@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS invalid-schemas-accepted (#145): a constraining facet may not be applied where a `simpleContent` restriction's base chain resolves to `xs:anySimpleType`, the ur simple type, which carries no constraining facets (XSD Part 2 §3.2.1); stZ010 (a complex type extending `xs:anySimpleType`, restricted by another that adds `minLength`) is now rejected. The base chain is followed through local complex types' own `simpleContent` bases; a base that bottoms at any other built-in, a simple type, or an external/unresolved type is left alone, and a restriction supplying its own nested `simpleType` base is skipped (the facet applies to that), so a facet on a string-, int-, or other real-typed simpleContent stays accepted. invalid-schemas-accepted 49 → 48, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group may not ADD an attribute the original does not declare (a superset is not a restriction, src-redefine / cos-ct-restricts); schM3 (redeclaring the original's attributes plus a new `b4`) is now rejected. The base is resolved by namespaced identity with a `{}name` chameleon fallback (a no-targetNamespace redefined schema is absorbed into the redefining namespace). To stay false-positive-free the added-attribute check runs only when the original's attribute set is fully known from its direct declarations (the original has no nested `attributeGroup` reference) AND the redefinition itself carries no `attributeGroup` reference: a self-reference brings in the original's attributes, so declaring further ones alongside it is a valid redefine (W3C attgC007/attgC038/groupB018, and test75083/xsd003b), not an addition. invalid-schemas-accepted 51 → 49, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; five self-reference over-rejections surfaced by the gate were fixed before commit), no other bucket moved.
|
||||
|
||||
- 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.
|
||||
|
||||
@ -302,4 +302,69 @@ extension PureXML.Schema.XSDParser {
|
||||
forEachRestrictionInSimpleType(child, visit)
|
||||
}
|
||||
}
|
||||
|
||||
/// A constraining facet may not be applied where a `simpleContent` restriction's
|
||||
/// base chain resolves to `xs:anySimpleType`, the ur simple type, which carries no
|
||||
/// constraining facets (XSD Part 2 §3.2.1): e.g. a complex type extending
|
||||
/// `xs:anySimpleType`, restricted by another adding `minLength`. The chain is
|
||||
/// followed through local complex types' own simpleContent bases; a base that is a
|
||||
/// built-in other than anySimpleType, a simple-type, or external/unresolved is left
|
||||
/// alone, so no valid facet is rejected. A restriction supplying its own nested
|
||||
/// `simpleType` base is also skipped (the facet applies to that, not anySimpleType).
|
||||
static func anySimpleTypeFacetErrors(_ schema: XSDTree) -> [String] {
|
||||
let bindings = namespaceBindings(schema)
|
||||
let target = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace")
|
||||
var errors: [String] = []
|
||||
forEachSimpleContentRestriction(schema) { restriction in
|
||||
guard PureXML.Schema.XSDNode.firstChild(restriction, named: "simpleType") == nil,
|
||||
let base = PureXML.Schema.XSDNode.attribute(restriction, "base"),
|
||||
hasConstrainingFacet(restriction),
|
||||
simpleContentBaseIsAnySimpleType(base, schema, bindings, target, [])
|
||||
else { return }
|
||||
errors.append("a constraining facet may not be applied to anySimpleType-based simpleContent")
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func hasConstrainingFacet(_ restriction: XSDTree) -> Bool {
|
||||
PureXML.Schema.XSDNode.elementChildren(restriction)
|
||||
.compactMap(PureXML.Schema.XSDNode.localName)
|
||||
.contains { facetNames.contains($0) }
|
||||
}
|
||||
|
||||
/// Whether the `simpleContent` `base` chain bottoms at `xs:anySimpleType`. Follows a
|
||||
/// base naming a local complex type's `simpleContent` to that type's own base;
|
||||
/// stops (false) at any other built-in, a simple-type base, or an unresolved one.
|
||||
private static func simpleContentBaseIsAnySimpleType(
|
||||
_ base: String, _ schema: XSDTree, _ bindings: [String: String], _ target: String?, _ visited: Set<String>,
|
||||
) -> Bool {
|
||||
let (prefix, local) = splitQName(base)
|
||||
let uri = prefix.map { bindings[$0] } ?? bindings[""]
|
||||
if uri == xsdNamespace { return local == "anySimpleType" }
|
||||
guard uri == target, !visited.contains(local),
|
||||
let complexType = globalComplexType(named: local, schema),
|
||||
let simpleContent = PureXML.Schema.XSDNode.firstChild(complexType, named: "simpleContent"),
|
||||
let derivation = PureXML.Schema.XSDNode.firstChild(simpleContent, named: "extension")
|
||||
?? PureXML.Schema.XSDNode.firstChild(simpleContent, named: "restriction"),
|
||||
let baseRef = PureXML.Schema.XSDNode.attribute(derivation, "base")
|
||||
else { return false }
|
||||
return simpleContentBaseIsAnySimpleType(baseRef, schema, bindings, target, visited.union([local]))
|
||||
}
|
||||
|
||||
private static func globalComplexType(named name: String, _ schema: XSDTree) -> XSDTree? {
|
||||
PureXML.Schema.XSDNode.elementChildren(schema).first {
|
||||
PureXML.Schema.XSDNode.localName($0) == "complexType" && PureXML.Schema.XSDNode.attribute($0, "name") == name
|
||||
}
|
||||
}
|
||||
|
||||
private static func forEachSimpleContentRestriction(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
if local == "simpleContent", let restriction = PureXML.Schema.XSDNode.firstChild(node, named: "restriction") {
|
||||
visit(restriction)
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
forEachSimpleContentRestriction(child, visit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ extension PureXML.Schema.XSDParser {
|
||||
append(valueConstraintErrors(schema), at: schema, into: &findings)
|
||||
append(topLevelDeclarationErrors(schema), at: schema, into: &findings)
|
||||
append(nestedNamedDefinitionErrors(schema), at: schema, into: &findings)
|
||||
append(anySimpleTypeRestrictionErrors(schema), at: schema, into: &findings)
|
||||
append(anySimpleTypeRestrictionErrors(schema) + anySimpleTypeFacetErrors(schema), at: schema, into: &findings)
|
||||
append(emptyNamespaceErrors(schema), at: schema, into: &findings)
|
||||
return findings
|
||||
}
|
||||
|
||||
46
Tests/SchemaAnySimpleTypeFacetTests.swift
Normal file
46
Tests/SchemaAnySimpleTypeFacetTests.swift
Normal file
@ -0,0 +1,46 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// A constraining facet may not be applied where a `simpleContent` restriction's base
|
||||
/// chain resolves to `xs:anySimpleType`, the ur simple type, which carries no
|
||||
/// constraining facets (XSD Part 2 §3.2.1, W3C stZ010). A base that bottoms at a real
|
||||
/// built-in or a user simple type still admits its applicable facets.
|
||||
@Suite("anySimpleType simpleContent facet applicability")
|
||||
struct SchemaAnySimpleTypeFacetTests {
|
||||
private func compile(_ schema: String) throws -> PureXML.Schema.Document {
|
||||
try PureXML.Schema.Document(schema)
|
||||
}
|
||||
|
||||
private func chain(base: String, facet: String) -> String {
|
||||
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:complexType name='t1'><xs:simpleContent><xs:extension base='\(base)'/></xs:simpleContent></xs:complexType>"
|
||||
+ "<xs:complexType name='t2'><xs:simpleContent><xs:restriction base='t1'>\(facet)</xs:restriction></xs:simpleContent></xs:complexType>"
|
||||
+ "</xs:schema>"
|
||||
}
|
||||
|
||||
@Test("a facet on an anySimpleType-based simpleContent restriction is rejected")
|
||||
func test_facetOnAnySimpleTypeRejected() throws {
|
||||
#expect(throws: (any Error).self) {
|
||||
try compile(chain(base: "xs:anySimpleType", facet: "<xs:minLength value='1'/>"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("minLength on a string-based simpleContent restriction is valid")
|
||||
func test_facetOnStringAccepted() throws {
|
||||
_ = try compile(chain(base: "xs:string", facet: "<xs:minLength value='1'/>"))
|
||||
}
|
||||
|
||||
@Test("maxInclusive on an int-based simpleContent restriction is valid")
|
||||
func test_facetOnIntAccepted() throws {
|
||||
_ = try compile(chain(base: "xs:int", facet: "<xs:maxInclusive value='5'/>"))
|
||||
}
|
||||
|
||||
@Test("an anySimpleType extension with no facet restriction is valid")
|
||||
func test_anySimpleTypeWithoutFacetAccepted() throws {
|
||||
_ = try compile(
|
||||
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:complexType name='t1'><xs:simpleContent><xs:extension base='xs:anySimpleType'/></xs:simpleContent></xs:complexType>"
|
||||
+ "</xs:schema>",
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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 = 49
|
||||
private let knownSchemaInvalidAccepted = 48
|
||||
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