PureXML/Tests/SchemaXSITypeUnionMemberBlockTests.swift
Mihaela Mihaljevic 94e16b1d7d
fix(schema): block an xsi:type that reaches a union declared type through a member (#277)
cvc-elt.4.3 defers to cos-st-derived-OK, whose clause 2.2.4 makes a type
validly derived from a union when it is validly derived from one of the
union's member types. The block check followed only the named base chain,
so when the declared type was a union and the substitute reached it through
a member by a blocked derivation method, the check found no path and stayed
silent, wrongly accepting the substitution (W3C XSTS elemT074: block on the
element is restriction and the xsi:type restricts a union member).

Record a union's direct member types during schema compilation and follow a
derivation through a member when the base chain does not reach the union.
The member clause is existential: bar the substitution only when every
confirmed derivation path to a member is blocked, so a substitute that is
itself a member (its self-membership path uses no method) or reaches one
member by an unblocked method is accepted whatever other members it reaches
by a blocked method. The check adds a rejection only on a positively
confirmed path, so an unresolved, inline-anonymous, or nested-union member
leaves it silent: a bounded, disclosed under-rejection, never a false
positive.

Drops XSTS invalid-instances-accepted 15 to 14 with valid-schemas-rejected
and valid-instances-rejected held at 0 over the full archive. An adversarial
over-rejection review caught a first cut that order-shadowed the
self-membership path; the existential form rules it out and the case is a
regression test.
2026-06-24 21:12:35 +02:00

97 lines
5.1 KiB
Swift

import Testing
@testable import PureXML
/// cvc-elt.4.3 with cos-st-derived-OK clause 2.2.4: an `xsi:type` is validly
/// derived from a union declared type when it is validly derived from one of the
/// union's member types. The `{disallowed substitutions}` (`block`) applies to
/// the derivation method used to reach that member, so a substitute that reaches
/// the declared union through a member by a blocked method must be rejected, even
/// though the substitute is not on the union's own base chain.
///
/// The W3C XSTS instance test `elemT074` is exactly this shape: `block="restriction"`
/// on the element, declared type a union, and an `xsi:type` that restricts a union
/// member. Before the union-member derivation path was followed, the substitution
/// was accepted (a false negative); the block check found no base-chain path to the
/// union and stayed silent.
@Suite("xsi:type block through a union member")
struct SchemaXSITypeUnionMemberBlockTests {
/// `Member` restricts `A`; `A` is a member of the union `U`. The element
/// `e` is declared with the union type and `block` set to `elementBlock`.
private func schema(elementBlock: String) throws -> PureXML.Schema.Document {
try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root"><xs:complexType><xs:sequence>
<xs:element ref="e"/>
</xs:sequence></xs:complexType></xs:element>
<xs:element name="e" type="U" block="\(elementBlock)"/>
<xs:simpleType name="A">
<xs:restriction base="xs:int"><xs:minInclusive value="0"/></xs:restriction>
</xs:simpleType>
<xs:simpleType name="B">
<xs:restriction base="xs:string"/>
</xs:simpleType>
<xs:simpleType name="U"><xs:union memberTypes="A B"/></xs:simpleType>
<xs:simpleType name="Member">
<xs:restriction base="A"><xs:enumeration value="1"/></xs:restriction>
</xs:simpleType>
</xs:schema>
""")
}
private let instance = #"<root><e xsi:type="Member" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">1</e></root>"#
@Test("block='restriction' bars an xsi:type that restricts a union member")
func test_blockBarsRestrictionThroughMember() throws {
let errors = try schema(elementBlock: "restriction").validate(instance)
#expect(!errors.isEmpty, "the substitution reaches union member A by restriction, which is blocked")
}
@Test("block='#all' bars it too")
func test_blockAllBarsThroughMember() throws {
#expect(try !schema(elementBlock: "#all").validate(instance).isEmpty)
}
@Test("block='extension' does NOT bar a restriction-derived member (no false positive)")
func test_extensionBlockDoesNotBar() throws {
// The derivation through the member uses restriction, not extension, so an
// extension-only block leaves the valid substitution accepted.
let errors = try schema(elementBlock: "extension").validate(instance)
#expect(errors.isEmpty, "extension block must not reject a restriction-derived substitution")
}
@Test("no block accepts the substitution (no false positive)")
func test_noBlockAccepts() throws {
let errors = try schema(elementBlock: "").validate(instance)
#expect(errors.isEmpty, "with no block the union-member substitution is valid")
}
/// A substitute that IS a union member must be accepted even when it also
/// derives, by a blocked method, from another member listed before it: the
/// union member clause is existential (cos-st-derived-OK 2.2.4), and the
/// self-membership path uses no derivation method, so it is never blocked.
/// Regression for an over-rejection where the first derivable member shadowed
/// the later self-membership match.
@Test("a member that also descends from an earlier member is accepted (no false positive)")
func test_selfMembershipNotShadowedByEarlierMember() throws {
// U's members are P then Q, and Q restricts P. xsi:type="Q" is a member of
// U (valid), though it also reaches member P by the blocked restriction.
let document = try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root"><xs:complexType><xs:sequence>
<xs:element ref="e"/>
</xs:sequence></xs:complexType></xs:element>
<xs:element name="e" type="U" block="restriction"/>
<xs:simpleType name="P">
<xs:restriction base="xs:int"><xs:minInclusive value="0"/></xs:restriction>
</xs:simpleType>
<xs:simpleType name="Q">
<xs:restriction base="P"><xs:enumeration value="1"/></xs:restriction>
</xs:simpleType>
<xs:simpleType name="U"><xs:union memberTypes="P Q"/></xs:simpleType>
</xs:schema>
""")
let instance = #"<root><e xsi:type="Q" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">1</e></root>"#
#expect(try document.validate(instance).isEmpty, "Q is a member of U; self-membership uses no method and is not blocked")
}
}