diff --git a/CHANGELOG.md b/CHANGELOG.md index e21a396..1a44982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Schema/ComplexValidator.swift b/Sources/Schema/ComplexValidator.swift index 8c5de5e..94ca6cd 100644 --- a/Sources/Schema/ComplexValidator.swift +++ b/Sources/Schema/ComplexValidator.swift @@ -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 } diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift index 095f7fc..132713b 100644 --- a/Sources/Schema/ComplexValidatorDerivation.swift +++ b/Sources/Schema/ComplexValidatorDerivation.swift @@ -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 + } } diff --git a/Tests/SchemaSubstitutionBlockTests.swift b/Tests/SchemaSubstitutionBlockTests.swift index 06bca8c..4bbe6a0 100644 --- a/Tests/SchemaSubstitutionBlockTests.swift +++ b/Tests/SchemaSubstitutionBlockTests.swift @@ -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(""" + + + + + + + + + """) + func doc(_ type: String) -> String { + #"x"# + } + // 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) + } } diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 26f9be8..cd018dd 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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 {