From c80659d80dfbdf3b63444d114306bed78dcfdb49 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Thu, 18 Jun 2026 13:24:32 +0200 Subject: [PATCH] fix(schema): resolve the declared type through the element-ref chain for the list/union xsi:type check; invalid-instances-accepted 78 to 66 (#181, #147) The list/union xsi:type derivation check decided whether the declared type admits a list or union substitute from a single type-table lookup, so it never reached the concrete type of an element declared by reference (``): such an element's list/union xsi:type was wrongly accepted. declaredAdmitsAnySimpleType now resolves the declared type through the full element-ref / type-reference chain (resolvedDeclaredType, the type-returning companion to resolvedDeclaredTypeName) to the concrete .simple/.complex before deciding. A list or union xsi:type on a ref'd element whose declaration resolves to a more specific atomic type is now rejected; the chain is cycle-bounded and falls through to suppress (under-reject) when it cannot resolve, so a ur-typed, list/union-typed, or unresolvable declared type still admits the substitute. invalid-instances-accepted 78 to 66, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. An adversarial review exercised ref chains to anySimpleType/anyType/untyped declarations, named and aliased list/union declarations, a deep alias chain, cross-namespace imported refs, substitution-group heads, atomic restriction substitutes, and a self- referential alias cycle, and found no over-rejection. --- CHANGELOG.md | 2 +- .../Schema/ComplexValidatorDerivation.swift | 31 ++++++++++++------- Tests/SchemaListUnionXsiTypeTests.swift | 18 +++++++++++ Tests/XSTSSuiteTests.swift | 14 ++++----- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1cc87d..fd1e786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ 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 (#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. The declared type is resolved through the element-ref / type-reference chain, so the check also fires for an element reached via `` whose global declaration is atomic. 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 → 66, 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. diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift index 24012dd..4cb4fbd 100644 --- a/Sources/Schema/ComplexValidatorDerivation.swift +++ b/Sources/Schema/ComplexValidatorDerivation.swift @@ -52,22 +52,31 @@ extension PureXML.Schema.ComplexValidator { /// 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 { + let resolved: PureXML.Schema.ElementType? = switch declared { + case .simple, .complex: declared + case let .typeReference(reference): resolvedDeclaredType(reference) + } + switch resolved { 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) + default: return true } } - /// 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) + /// Follows an element-ref / type-reference chain (`element:foo` -> concrete type) + /// to the concrete declared type (a `.simple` or `.complex`), or nil when the + /// chain does not resolve. The name-returning ``resolvedDeclaredTypeName`` walks + /// the same chain for the backbone key. + private func resolvedDeclaredType(_ reference: String) -> PureXML.Schema.ElementType? { + var key = reference + var resolved = types[key] ?? Self.resolveNamedType(key, in: types) + var steps = 0 + while case let .typeReference(next)? = resolved, steps <= types.count { + key = next + resolved = types[key] ?? Self.resolveNamedType(key, in: types) + steps += 1 + } + return resolved } /// Whether a simple type is a list or union (deriving only from `anySimpleType`). diff --git a/Tests/SchemaListUnionXsiTypeTests.swift b/Tests/SchemaListUnionXsiTypeTests.swift index e34cf4b..ed638b8 100644 --- a/Tests/SchemaListUnionXsiTypeTests.swift +++ b/Tests/SchemaListUnionXsiTypeTests.swift @@ -40,4 +40,22 @@ struct SchemaListUnionXsiTypeTests { #expect(try schema(declaredType: "xs:anySimpleType").validate(doc("myList", "1 2")).isEmpty) #expect(try schema(declaredType: "xs:anySimpleType").validate(doc("myUnion", "x")).isEmpty) } + + /// The declared type is resolved through the element-ref chain: a list/union + /// xsi:type on an element reached via `` (whose global + /// declaration is atomic) is still rejected. + @Test("a list/union xsi:type on a ref'd atomic element is rejected") + func test_listUnionOnReferencedAtomicRejected() throws { + let schema = try PureXML.Schema.Document(""" + + + + + + + + """) + let xml = #"1 2"# + #expect(try !schema.validate(xml).isEmpty) + } } diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 97dd0d8..3cc8191 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -150,13 +150,13 @@ struct XSTSSuiteTests { private let knownSchemaValidRejected = 0 private let knownSchemaInvalidAccepted = 69 private let knownInstanceValidRejected = 0 - /// Instance bucket: xsi:type must derive from the declared type (133 -> 107); - /// 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 + /// Instance bucket: xsi:type must derive from the declared type (133 -> 107); 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 declared type, resolved through an element-ref chain (81 -> 66). + private let knownInstanceInvalidAccepted = 66 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws {