diff --git a/CHANGELOG.md b/CHANGELOG.md index 5083afc..a1cc87d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift index ea0c66e..24012dd 100644 --- a/Sources/Schema/ComplexValidatorDerivation.swift +++ b/Sources/Schema/ComplexValidatorDerivation.swift @@ -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. diff --git a/Tests/SchemaListUnionXsiTypeTests.swift b/Tests/SchemaListUnionXsiTypeTests.swift new file mode 100644 index 0000000..e34cf4b --- /dev/null +++ b/Tests/SchemaListUnionXsiTypeTests.swift @@ -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(""" + + + + + + + """) + } + + private func doc(_ type: String, _ value: String) -> String { + #"\#(value)"# + } + + @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) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 83f9715..97dd0d8 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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 {