PureXML/Tests/SchemaIdentityFieldSimpleTypeTests.swift
Mihaela Mihaljevic 8f2725b62a
fix(schema): reject an identity-constraint field that selects a non-simple node (#381)
XSD 1.0 §3.11.4 (cvc-identity-constraint.3) requires each key/keyref/unique
field to identify a node of simple type. A field that reaches an element
declared with no type (the ur-type anyType) or an inline complex type without
simple content does not, so the instance is invalid; the reference (libxml2)
rejects it identically. PureXML accepted such instances because its identity
validator could resolve a field target's type only for a global element reached
by a direct-child name, not for a local element reached through a path (the W3C
XSTS case idZ010 nests anyType fields several levels deep, e.g. a key field
./b/c where c has no type).

A compile-time pass (XSDParser.identityNonSimpleFieldKeys) resolves each field's
target element through its selector and field paths, by element-name steps, and
records the fields whose declared target is provably non-simple. The validator
rejects those at runtime unless the instance gives the node a simple type
through xsi:type, since cvc reads the node's governing type (a text-only anyType
element with xsi:type="xsd:int" is a valid field). The resolution is
conservative: any wildcard, descendant axis, predicate, ref, or named
non-builtin type leaves the field unflagged, so a valid field is never rejected.
The one residual divergence path, two same-name child elements of differing
simplicity in one inline model, is an Element-Declarations-Consistent violation
the compiler already rejects.

Verified over the full W3C XSTS corpus in release: invalid-instances-accepted
8 -> 7 (idZ010 now rejects), valid-instances-rejected held at 0 across all 14383
groups (no false positive), full suite (1837 tests), wasm, and lint all green.
An adversarial critic that read every file and probed for a false positive could
not construct one; the failure mode is under-rejection, the safe direction.
2026-06-29 09:08:25 +02:00

84 lines
3.8 KiB
Swift

import Testing
@testable import PureXML
/// cvc-identity-constraint.3: a `key`/`keyref`/`unique` field must identify a node
/// of simple type. A field that reaches an element whose declared type is the
/// ur-type `anyType` (no type) or a complex type without simple content is invalid,
/// unless the instance gives the node a simple type through `xsi:type`. Mirrors the
/// W3C XSTS idZ010 shape (a `key`/`keyref` whose fields select no-type elements
/// nested through a path), the case PureXML previously accepted.
@Suite("Identity-constraint field must select a simple-typed node")
struct SchemaIdentityFieldSimpleTypeTests {
/// idZ010 in miniature: `root/a` carries a `key` whose field path reaches `c`,
/// declared with no type (so `anyType`). The field is not simple, so the
/// instance is invalid even though the value would otherwise be a fine key.
private func anyTypeFieldSchema() throws -> PureXML.Schema.Document {
try PureXML.Schema.Document("""
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="root">
<xsd:complexType><xsd:sequence>
<xsd:element name="a" maxOccurs="unbounded">
<xsd:complexType><xsd:sequence>
<xsd:element name="b">
<xsd:complexType><xsd:sequence>
<xsd:element name="c"/>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
<xsd:key name="k">
<xsd:selector xpath="a"/>
<xsd:field xpath="./b/c"/>
</xsd:key>
</xsd:element>
</xsd:schema>
""")
}
@Test("a key field reaching a no-type (anyType) element is rejected")
func test_anyTypeFieldRejected() throws {
let schema = try anyTypeFieldSchema()
let errors = try schema.validate("<root><a><b><c>1</c></b></a></root>")
#expect(!errors.isEmpty)
#expect(errors.contains { $0.reason.contains("simple content") })
}
@Test("a simple-typed field at the same path is accepted")
func test_simpleTypedFieldAccepted() throws {
let schema = try PureXML.Schema.Document("""
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="root">
<xsd:complexType><xsd:sequence>
<xsd:element name="a" maxOccurs="unbounded">
<xsd:complexType><xsd:sequence>
<xsd:element name="b">
<xsd:complexType><xsd:sequence>
<xsd:element name="c" type="xsd:int"/>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
<xsd:key name="k"><xsd:selector xpath="a"/><xsd:field xpath="./b/c"/></xsd:key>
</xsd:element>
</xsd:schema>
""")
#expect(try schema.validate("<root><a><b><c>1</c></b></a><a><b><c>2</c></b></a></root>").isEmpty)
}
@Test("an anyType field rescued by a simple xsi:type is accepted")
func test_anyTypeRescuedByXsiType() throws {
// The element is declared anyType, but the instance gives it xsi:type=int,
// so its governing type is simple and the field is valid (cvc reads the
// governing type). This is the guard that keeps the check from a false
// positive on a legitimately simple-at-runtime node.
let schema = try anyTypeFieldSchema()
let xml = """
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\
<a><b><c xsi:type="xsd:int">1</c></b></a></root>
"""
#expect(try schema.validate(xml).isEmpty)
}
}