fix(schema): an xsi:type must be derived from the declared type (cvc-elt.4.3.2.1); invalid-instances-accepted 133 to 107 (#181)
First instance-bucket reduction of the campaign. An instance xsi:type naming a complex type must be derived from the element's declared type. notDerivedSubstitutionError flags a complex substitute on a different branch of the hierarchy; previously only a derived-but-blocked substitution was caught (blockedSubstitutionError returns nil when there is no derivation path), so a non-derived xsi:type was silently accepted (the test006 cluster, 32 instances, and more). Conservatively scoped after a broader first cut took 20 false positives (now reverted): it fires only when the substitute resolves to a COMPLEX type and both substitute and declared names are in the derivation backbone with no path between them. Simple/list/union substitutes, ur-types, and unresolvable references are left alone -- under-reject, never over-reject. An adversarial review confirmed the conservative guards eliminate the prior false positives; the one residual is the pre-existing bare-local-name keying of the derivation backbone (shared with blockedSubstitutionError), disclosed in the code, corpus-absent. The three xsi:type override checks (blocked / not-derived / abstract) are folded into one xsiTypeOverrideError helper to keep validateChild under the complexity limit. XSTS: invalid-instances-accepted 133 to 107 (26 caught), valid-instances-rejected and valid-schemas-rejected both held at 0, no other bucket moved.
This commit is contained in:
parent
0994f5dd2a
commit
8e873dc929
@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS invalid-instances-accepted (#181, #147): an instance `xsi:type` naming a complex type must be derived from the element's declared type (`cvc-elt.4.3.2.1`). A complex `xsi:type` on a different branch of the type hierarchy (not derived from the declared type) is now rejected, where previously only a derived-but-blocked substitution was caught. Conservatively scoped to substitutes that resolve to a complex type recorded in the derivation backbone, so simple/list/union substitutes and unresolvable references are left alone (no over-rejection). invalid-instances-accepted 133 → 107, no false positive (valid-instances-rejected held at 0, valid-schemas-rejected held at 0), no other bucket moved. Disclosed bound: the derivation backbone is keyed by bare local name (shared with the existing block-substitution rule), so a cross-namespace same-local-name collision could in principle be misjudged; the corpus exercises none, and the holistic fix is to key the backbone by namespaced type identity.
|
||||
|
||||
- XSTS invalid-schemas-accepted (#145): an `xs:include` and `xs:redefine` require a `schemaLocation` attribute (only `xs:import`'s is optional); one declared without it is rejected. invalid-schemas-accepted 71 → 69, no false positive (valid-schemas-rejected held at 0; a required-attribute check with no name resolution cannot over-reject), no other bucket moved.
|
||||
|
||||
- XSTS invalid-schemas-accepted (#161): a schema referenced by `xs:redefine` must have the same `targetNamespace` as the redefining schema, or none (chameleon), exactly as for `xs:include`; redefining a different-namespace schema is rejected. The existing include-namespace-matching check now also covers `redefine`, selecting the loaded redefined schema document (not the redefine wrapper) so its real target namespace is compared. invalid-schemas-accepted 74 → 71, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed same-namespace, chameleon, and no-namespace redefines are not flagged), no other bucket moved.
|
||||
|
||||
@ -273,12 +273,8 @@ public extension PureXML.Schema {
|
||||
// reference so re-entry does not re-read the same xsi:type. A `block` on
|
||||
// the declared type can forbid the substitution; an abstract declared
|
||||
// type requires one.
|
||||
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) {
|
||||
errors.append(blocked)
|
||||
return
|
||||
}
|
||||
if case let .typeReference(name) = declared, let missing = abstractTypeError(named: name, child: child, at: path) {
|
||||
errors.append(missing)
|
||||
if let xsiError = xsiTypeOverrideError(declared: declared, child: child, at: path) {
|
||||
errors.append(xsiError)
|
||||
return
|
||||
}
|
||||
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
|
||||
|
||||
@ -1,4 +1,18 @@
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
/// The first `xsi:type` override error for `child`, if any: a substitution
|
||||
/// blocked by `block`, a substitute not validly derived from the declared type
|
||||
/// (cvc-elt.4.3.2.1), or a missing `xsi:type` on an abstract declared type.
|
||||
func xsiTypeOverrideError(
|
||||
declared: PureXML.Schema.ElementType,
|
||||
child: PureXML.Model.Element,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) { return blocked }
|
||||
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path) { return notDerived }
|
||||
if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The error when an element's declared type is an abstract complex type and
|
||||
/// the element supplies no `xsi:type` to name a concrete derived type. An
|
||||
/// abstract type cannot itself be the type of an instance element.
|
||||
@ -61,4 +75,68 @@ extension PureXML.Schema.ComplexValidator {
|
||||
at: path,
|
||||
)
|
||||
}
|
||||
|
||||
/// cvc-elt.4.3.2.1: an `xsi:type` must resolve to a type DERIVED from the
|
||||
/// element's declared type. This flags ONLY the case where both the declared
|
||||
/// type and the substitute resolve to COMPLEX types recorded in the derivation
|
||||
/// backbone and there is no derivation path between them (a different branch of
|
||||
/// the hierarchy). It deliberately does NOT touch a simple/list/union substitute,
|
||||
/// a substitute or declared type absent from the backbone, or the ur-types: there
|
||||
/// the relationship cannot be confirmed from the complex-type backbone alone, so
|
||||
/// the rule stays silent (under-reject) rather than risk a false positive.
|
||||
///
|
||||
/// Disclosed bound: the derivation backbone (`typeDerivation`) is keyed by bare
|
||||
/// local name, shared with `blockedSubstitutionError`. Two imported complex types
|
||||
/// with the same local name in different namespaces collide in one slot, so a
|
||||
/// cross-namespace substitution could in principle be misjudged. This is a
|
||||
/// pre-existing, subsystem-wide limitation (not introduced here); the holistic
|
||||
/// fix is to key the backbone by namespaced type identity and resolve `xsi:type`
|
||||
/// through `xsiTypeReference`. The XSTS corpus exercises no such collision.
|
||||
func notDerivedSubstitutionError(
|
||||
declared: PureXML.Schema.ElementType,
|
||||
child: PureXML.Model.Element,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
guard case let .typeReference(reference) = declared,
|
||||
let substitute = Self.xsiTypeName(child),
|
||||
case .complex? = Self.resolveNamedType(substitute, in: types)
|
||||
else { return nil }
|
||||
let substituteName = PureXML.Schema.XSDParser.bareTypeLocalName(substitute)
|
||||
let declaredName = resolvedDeclaredTypeName(reference)
|
||||
guard substituteName != declaredName,
|
||||
typeDerivation[substituteName] != nil,
|
||||
typeDerivation[declaredName] != nil || isComplexBackboneRoot(declaredName),
|
||||
PureXML.Schema.XSDParser.derivationMethods(from: substituteName, to: declaredName, typeDerivation) == nil
|
||||
else { return nil }
|
||||
return PureXML.Validation.ValidationError(
|
||||
reason: "xsi:type '\(substitute)' is not validly derived from the declared type '\(declaredName)' of '\(child.name.localName)'",
|
||||
at: path,
|
||||
)
|
||||
}
|
||||
|
||||
/// A complex type that heads a derivation backbone (some recorded type derives
|
||||
/// from it) even though it records no base of its own, so it is a legitimate
|
||||
/// declared-type target for the not-derived check.
|
||||
private func isComplexBackboneRoot(_ name: String) -> Bool {
|
||||
guard name != "anyType", name != "anySimpleType" else { return false }
|
||||
return typeDerivation.values.contains { $0.base == name }
|
||||
}
|
||||
|
||||
/// Follows an element-ref / type-reference chain (`element:foo` -> concrete type)
|
||||
/// to the declared type's bare local name, which the derivation backbone is
|
||||
/// keyed by.
|
||||
private func resolvedDeclaredTypeName(_ reference: String) -> String {
|
||||
var key = reference
|
||||
var name = PureXML.Schema.XSDParser.bareTypeLocalName(reference)
|
||||
var steps = 0
|
||||
while steps <= types.count {
|
||||
guard let resolved = types[key] ?? Self.resolveNamedType(key, in: types),
|
||||
case let .typeReference(next) = resolved
|
||||
else { break }
|
||||
key = next
|
||||
name = PureXML.Schema.XSDParser.bareTypeLocalName(next)
|
||||
steps += 1
|
||||
}
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,4 +39,28 @@ struct SchemaSubstitutionBlockTests {
|
||||
func test_memberAllowedWithoutBlock() throws {
|
||||
#expect(try schema(headBlock: "").validate(withMember).isEmpty)
|
||||
}
|
||||
|
||||
/// cvc-elt.4.3.2.1: an xsi:type naming a complex type must be derived from the
|
||||
/// element's declared type. A complex type on a different branch of the
|
||||
/// hierarchy is rejected; the declared type itself is valid.
|
||||
@Test("an xsi:type must be derived from the element's declared type")
|
||||
func test_xsiTypeMustDeriveFromDeclared() throws {
|
||||
let schema = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="urn:s" targetNamespace="urn:s" elementFormDefault="qualified">
|
||||
<xs:complexType name="B"><xs:sequence><xs:element name="f" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
<xs:complexType name="De"><xs:complexContent><xs:extension base="B"/></xs:complexContent></xs:complexType>
|
||||
<xs:complexType name="Dr"><xs:complexContent><xs:restriction base="B">
|
||||
<xs:sequence><xs:element name="f" type="xs:string"/></xs:sequence>
|
||||
</xs:restriction></xs:complexContent></xs:complexType>
|
||||
<xs:element name="e" type="De"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
func doc(_ type: String) -> String {
|
||||
#"<e xmlns="urn:s" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="\#(type)"><f>x</f></e>"#
|
||||
}
|
||||
// Dr (restriction of B) is on a different branch, not derived from De: invalid.
|
||||
#expect(try !schema.validate(doc("Dr")).isEmpty)
|
||||
// The declared type itself is valid.
|
||||
#expect(try schema.validate(doc("De")).isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +150,11 @@ struct XSTSSuiteTests {
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 69
|
||||
private let knownInstanceValidRejected = 0
|
||||
private let knownInstanceInvalidAccepted = 133
|
||||
/// Instance bucket: an xsi:type naming a complex type must be derived from the
|
||||
/// element's declared type (cvc-elt.4.3.2.1); a non-derived complex substitute is
|
||||
/// rejected, conservatively scoped to the complex-type derivation backbone so no
|
||||
/// valid instance is over-rejected. 133 -> 107.
|
||||
private let knownInstanceInvalidAccepted = 107
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user