fix(schema): reject a list/union xsi:type on a non-ur-typed element; invalid-instances-accepted 81 to 78 (#181, #147)
A list or union simple type is derived only from xsd:anySimpleType (XSD Part 2), so an xsi:type naming a list or union can validly stand in (cvc-elt.4.3.2.1) only for an element whose declared type is a ur-type (anySimpleType/anyType). Naming one on an element of a more specific atomic or complex type, such as xsi:type="myList" on an xs:int element, is not a valid derivation and is now rejected. listUnionSubstitutionError flags the case; declaredAdmitsAnySimpleType suppresses it when the declared type is a ur-type, is itself a list or union (whose inline identity cannot be matched), or is an unresolvable reference, so the check biases to under-reject and never over-rejects. A genuinely derived restriction substitute, a list/union under an anySimpleType declaration, and a restriction of a list (which keeps list variety) are all left accepted. This widening became safe once the derivation backbone was keyed by namespaced identity (#186); the conservative atomic and built-in not-derived cases remain under-rejected pending the full type-identity relation. invalid-instances-accepted 81 to 78, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. An adversarial review exercised a list-restriction substitute, a union declared type, ur-type and typeReference-chain declarations, and cross-namespace list types, and found no over-rejection.
This commit is contained in:
parent
1fe670581f
commit
05a0aa4305
@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS invalid-instances-accepted (#181, #147): an `xsi:type` naming a list or union simple type is now rejected unless the element's declared type is a ur-type (`anySimpleType`/`anyType`). A list or union type derives only from `anySimpleType` (XSD Part 2), so it cannot validly substitute for an element of any more specific atomic or complex type (`cvc-elt.4.3.2.1`); naming one on, say, an `xs:int`-declared element is invalid. A genuinely derived (restriction) substitute, a list/union under an `anySimpleType` declaration, and a declared type that is itself a list or union are all left alone, so no valid instance is newly rejected. This widening became safe once the derivation backbone was keyed by namespaced identity (#186); the conservative atomic/built-in not-derived cases remain under-rejected pending the full type-identity relation. invalid-instances-accepted 81 → 78, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#186, #147): the instance-validity derivation and substitution tables are now keyed by namespaced identity (`{ns}local`) instead of bare local name, so two same-local-name types or elements in different namespaces no longer collide. `DerivationTables` keeps its bare-keyed fields for the schema-validity machinery (Particle-Valid-Restriction and the consistency rules) and gains parallel namespaced fields built in the same pass; only the instance consumers (the `xsi:type` blocked/not-derived/abstract checks, the substitution-group closure, and `referenceParticle`) read the namespaced tables, reducing a `type:{ns}local` reference, a bare name, or an element ref to a `{ns}local` key. A key miss stays permissive (no constraint), so the re-keying cannot over-reject; the single-document corpus behaviour is unchanged. With the namespaced backbone in place, a schema's `blockDefault` now supplies the `{disallowed substitutions}` (`block`) of every element and complex type that states none (own `block` overrides; `block=""` clears; intersected with the methods each component admits), without one namespace's defaults shadowing another's. invalid-instances-accepted 89 → 81, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0; an adversarial review exercised same- and cross-namespace substitutions, no-targetNamespace keys, transitive substitution groups, and blockDefault override cases), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#147): a complex type's effective attribute wildcard is now the INTERSECTION of the `anyAttribute` wildcards it draws from its own declaration and each referenced attribute group (XSD 1.0 `cos-aw-intersect`), not their union. An attribute is admitted only if every source admits it, so two attribute groups constraining disjoint namespaces (e.g. `urn:a` and `urn:b`) admit nothing and an attribute in either is rejected. A type with a single `anyAttribute` source is unchanged (intersection with no other source is the lone wildcard), and a complexContent extension's union with its base wildcard is unaffected. New `Wildcard.intersection` covers the namespace cases (`##any` identity, enumerated set intersection, enumerated-with-`##other` exclusion) and keeps the less restrictive result for the inexpressible `##other`/`##other` and mismatched `processContents` cases, so the change never over-rejects. invalid-instances-accepted 94 → 89, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#147): identity-constraint (`unique`/`key`/`keyref`) field values are now compared across the decimal-derived numeric family, so two fields typed by different members of that family with the same number collide. A field typed `xs:decimal` with value `1` and a field typed `xs:unsignedByte` with value `1` (e.g. supplied via `xsi:type`) are the same value and violate a `unique`, where previously a difference in declared base type made them compare unequal. The unification is confined to `xs:decimal` and the integer types, which share one value space; distinct primitive spaces (a number versus a string, or `float` versus `double`) are never unified, so no valid instance is newly rejected. invalid-instances-accepted 95 → 94, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
@ -16,10 +16,68 @@ extension PureXML.Schema.ComplexValidator {
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked }
|
||||
if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived }
|
||||
if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return listUnion }
|
||||
if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// cvc-elt.4.3.2.1 for a list or union `xsi:type`: a list or union simple type
|
||||
/// is derived only from `anySimpleType`, so it can validly stand in only for an
|
||||
/// element whose declared type is a ur-type (`anySimpleType`/`anyType`). Naming a
|
||||
/// list or union type on an element of any more specific atomic or complex type
|
||||
/// is not a valid derivation. When the declared type is itself a list or union
|
||||
/// the check stays silent (an inline list/union identity cannot be matched), so
|
||||
/// it biases to under-reject and never over-rejects.
|
||||
func listUnionSubstitutionError(
|
||||
declared: PureXML.Schema.ElementType,
|
||||
child: PureXML.Model.Element,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
namespaceBindings: [String: String],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
guard let label = Self.xsiTypeName(child),
|
||||
let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
|
||||
case let .simple(substitute)? = Self.resolveNamedType(reference, in: types),
|
||||
Self.isListOrUnion(substitute),
|
||||
!declaredAdmitsAnySimpleType(declared)
|
||||
else { return nil }
|
||||
return PureXML.Validation.ValidationError(
|
||||
reason: "xsi:type '\(label)' is a list or union type not validly derived from the declared type of '\(child.name.localName)'",
|
||||
at: path,
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether a list or union substitute may validly stand in for `declared`: only
|
||||
/// the ur-types (`anySimpleType`/`anyType`) admit one. A declared type that is
|
||||
/// itself a list or union, or an unresolvable reference, is also treated as
|
||||
/// admitting, so the list/union check stays silent rather than risk a false
|
||||
/// positive on an inline or unknown declared type.
|
||||
private func declaredAdmitsAnySimpleType(_ declared: PureXML.Schema.ElementType) -> Bool {
|
||||
switch declared {
|
||||
case let .simple(type): return type.isAnySimpleType || Self.isListOrUnion(type)
|
||||
case let .complex(type): return Self.isUrType(type)
|
||||
case let .typeReference(reference):
|
||||
guard case let .simple(type)? = Self.resolveNamedType(reference, in: types) else {
|
||||
if case .complex? = Self.resolveNamedType(reference, in: types) { return urReference(reference) }
|
||||
return true
|
||||
}
|
||||
return type.isAnySimpleType || Self.isListOrUnion(type)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a reference resolving to a complex type is the complex ur-type.
|
||||
private func urReference(_ reference: String) -> Bool {
|
||||
guard case let .complex(type)? = Self.resolveNamedType(reference, in: types) else { return false }
|
||||
return Self.isUrType(type)
|
||||
}
|
||||
|
||||
/// Whether a simple type is a list or union (deriving only from `anySimpleType`).
|
||||
static func isListOrUnion(_ type: PureXML.Schema.SimpleType) -> Bool {
|
||||
switch type.variety {
|
||||
case .atomic: false
|
||||
case .list, .union: true
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
43
Tests/SchemaListUnionXsiTypeTests.swift
Normal file
43
Tests/SchemaListUnionXsiTypeTests.swift
Normal file
@ -0,0 +1,43 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// cvc-elt.4.3.2.1 for a list or union `xsi:type`: a list or union simple type
|
||||
/// derives only from `anySimpleType`, so it can validly stand in only for an
|
||||
/// element whose declared type is a ur-type. Naming a list or union type on an
|
||||
/// element of a more specific atomic type is not a valid derivation and is
|
||||
/// rejected; a genuinely derived (restriction) substitute, or a list/union under
|
||||
/// an `anySimpleType` declaration, is still accepted.
|
||||
@Suite("list/union xsi:type derivation")
|
||||
struct SchemaListUnionXsiTypeTests {
|
||||
private func schema(declaredType: String) throws -> PureXML.Schema.Document {
|
||||
try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="e" type="\(declaredType)"/>
|
||||
<xs:simpleType name="myList"><xs:list itemType="xs:int"/></xs:simpleType>
|
||||
<xs:simpleType name="myUnion"><xs:union memberTypes="xs:int xs:string"/></xs:simpleType>
|
||||
<xs:simpleType name="myInt"><xs:restriction base="xs:int"><xs:minInclusive value="0"/></xs:restriction></xs:simpleType>
|
||||
</xs:schema>
|
||||
""")
|
||||
}
|
||||
|
||||
private func doc(_ type: String, _ value: String) -> String {
|
||||
#"<e xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="\#(type)">\#(value)</e>"#
|
||||
}
|
||||
|
||||
@Test("a list or union xsi:type on an int-declared element is rejected")
|
||||
func test_listUnionOnAtomicRejected() throws {
|
||||
#expect(try !schema(declaredType: "xs:int").validate(doc("myList", "1 2")).isEmpty)
|
||||
#expect(try !schema(declaredType: "xs:int").validate(doc("myUnion", "x")).isEmpty)
|
||||
}
|
||||
|
||||
@Test("a valid restriction xsi:type on an int-declared element is accepted")
|
||||
func test_restrictionAccepted() throws {
|
||||
#expect(try schema(declaredType: "xs:int").validate(doc("myInt", "5")).isEmpty)
|
||||
}
|
||||
|
||||
@Test("a list or union xsi:type under anySimpleType is accepted")
|
||||
func test_listUnionUnderUrTypeAccepted() throws {
|
||||
#expect(try schema(declaredType: "xs:anySimpleType").validate(doc("myList", "1 2")).isEmpty)
|
||||
#expect(try schema(declaredType: "xs:anySimpleType").validate(doc("myUnion", "x")).isEmpty)
|
||||
}
|
||||
}
|
||||
@ -151,12 +151,12 @@ struct XSTSSuiteTests {
|
||||
private let knownSchemaInvalidAccepted = 69
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket: xsi:type must derive from the declared type (133 -> 107);
|
||||
/// block="substitution" bars all members even for an untyped head (107 -> 101);
|
||||
/// `xsd:anyType` wildcards are `lax` (101 -> 95); identity fields compare in the
|
||||
/// decimal value space (95 -> 94); a type's attribute wildcard is the INTERSECTION
|
||||
/// of its sources (94 -> 89); `blockDefault` supplies an element's/type's
|
||||
/// `block` when it states none, after re-keying derivation by namespace (89 -> 81).
|
||||
private let knownInstanceInvalidAccepted = 81
|
||||
/// block="substitution" bars members even for an untyped head (107 -> 101); ur-type
|
||||
/// `xsd:anyType` wildcards are `lax` (101 -> 95); identity fields compare in the decimal
|
||||
/// value space (95 -> 94); attribute wildcard is the INTERSECTION of its sources (94 -> 89);
|
||||
/// `blockDefault` supplies a missing `block` after re-keying derivation by namespace
|
||||
/// (89 -> 81); a list/union xsi:type is not derived from a non-ur type (81 -> 78).
|
||||
private let knownInstanceInvalidAccepted = 78
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user