PureXML/Sources/Schema/XSDValidations.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

127 lines
6.8 KiB
Swift

public extension PureXML.Validation {
/// The compiled-schema context the XSD validations read: the named-type table
/// a content model resolves against, the identity constraints, and the root
/// element's declared type.
struct XSDContext {
public let types: [String: PureXML.Schema.ElementType]
public let constraints: [String: [PureXML.Schema.IdentityConstraint]]
public let rootDeclaration: PureXML.Schema.ElementType?
public let nillableElements: Set<String>
public let elementConstraints: [String: PureXML.Schema.ValueConstraint]
public let abstractTypes: Set<String>
public let typeBlock: [String: Set<PureXML.Schema.DerivationMethod>]
public let elementBlock: [String: Set<PureXML.Schema.DerivationMethod>]
public let typeDerivation: [String: PureXML.Schema.TypeDerivation]
public let unionMembers: [String: [String]]
public let globalAttributes: [String: PureXML.Schema.AttributeUse]
public let identityFieldTypes: [String: PureXML.Schema.SimpleType]
public let identityFieldConstraints: [String: PureXML.Schema.ValueConstraint]
public init(
types: [String: PureXML.Schema.ElementType],
constraints: [String: [PureXML.Schema.IdentityConstraint]],
rootDeclaration: PureXML.Schema.ElementType?,
nillableElements: Set<String> = [],
elementConstraints: [String: PureXML.Schema.ValueConstraint] = [:],
abstractTypes: Set<String> = [],
typeBlock: [String: Set<PureXML.Schema.DerivationMethod>] = [:],
elementBlock: [String: Set<PureXML.Schema.DerivationMethod>] = [:],
typeDerivation: [String: PureXML.Schema.TypeDerivation] = [:],
unionMembers: [String: [String]] = [:],
globalAttributes: [String: PureXML.Schema.AttributeUse] = [:],
identityFieldTypes: [String: PureXML.Schema.SimpleType] = [:],
identityFieldConstraints: [String: PureXML.Schema.ValueConstraint] = [:],
) {
self.types = types
self.constraints = constraints
self.rootDeclaration = rootDeclaration
self.nillableElements = nillableElements
self.elementConstraints = elementConstraints
self.abstractTypes = abstractTypes
self.typeBlock = typeBlock
self.elementBlock = elementBlock
self.typeDerivation = typeDerivation
self.unionMembers = unionMembers
self.globalAttributes = globalAttributes
self.identityFieldTypes = identityFieldTypes
self.identityFieldConstraints = identityFieldConstraints
}
}
}
public extension PureXML.Validation {
/// ``XSDContext``, in the OpenAPIKit idiom: each rule is a named, removable
/// ``Validation`` value with a positive description, and the ``validator()``
/// composes them; failures are located ``ValidationError``s.
///
/// Scope note (the #101 decision). XSD content validation is *one* recursive
/// ``Validation`` (``contentValidity``) rather than a separate rule per
/// constraint, because XSD is a recursive type system: a child's type is
/// resolved from its parent's content model and position, then rewritten by
/// `xsi:type` and the derivation controls mid-walk. The constraint categories
/// (attributes, content model, derivation, nillability, fixed values) are
/// interdependent through that resolution, so they cannot be split into
/// independent rules without either changing semantics or duplicating the
/// recursion. This is the sanctioned multi-error form (the rule's `check`
/// returns one located error per violation). Each category is isolation-tested
/// through crafted inputs so it is verifiable on its own, which is the
/// testability the idiom asks for. Identity constraints, which *are*
/// independent, are a separate rule (``identityConstraints``).
enum XSD {
/// The document element is valid against its declared XSD type.
static var contentValidity: Validation<PureXML.Model.Node, XSDContext> {
.init(
description: "The document element is valid against its XSD type",
check: { context in
guard case let .element(root) = context.subject, let declaration = context.document.rootDeclaration else {
return []
}
let schema = context.document
let validator = PureXML.Schema.ComplexValidator(
types: schema.types,
globalAttributes: schema.globalAttributes,
nillableElements: schema.nillableElements,
elementConstraints: schema.elementConstraints,
abstractTypes: schema.abstractTypes,
typeBlock: schema.typeBlock,
elementBlock: schema.elementBlock,
typeDerivation: schema.typeDerivation,
unionMembers: schema.unionMembers,
)
var errors = validator.validate(root, as: declaration, at: [.element(root.name.description)])
// Document-scoped ID uniqueness and IDREF resolution, gathered
// during the typed walk above, reported once the tree is seen.
errors += validator.idErrors()
return errors
},
when: { $0.codingPath.isEmpty },
)
}
/// The document's XSD identity constraints (`unique`, `key`, `keyref`) hold.
static var identityConstraints: Validation<PureXML.Model.Node, XSDContext> {
.init(
description: "XSD identity constraints hold",
check: { context -> [PureXML.Validation.ValidationError] in
guard case let .element(root) = context.subject else { return [] }
return PureXML.Schema.IdentityValidator(
constraints: context.document.constraints,
fieldTypes: context.document.identityFieldTypes,
fieldConstraints: context.document.identityFieldConstraints,
types: context.document.types,
).validate(PureXML.Model.TreeNode(context.subject), at: [.element(root.name.description)])
},
when: { $0.codingPath.isEmpty },
)
}
/// A validator combining the content-model and identity-constraint rules.
static func validator() -> Validator<XSDContext> {
Validator<XSDContext>.defaults(
nonReference: [AnyValidation(contentValidity)],
reference: [AnyValidation(identityConstraints)],
)
}
}
}