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.
131 lines
7.1 KiB
Swift
131 lines
7.1 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 let identityNonSimpleFields: Set<String>
|
|
|
|
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] = [:],
|
|
identityNonSimpleFields: Set<String> = [],
|
|
) {
|
|
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
|
|
self.identityNonSimpleFields = identityNonSimpleFields
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
nonSimpleFields: context.document.identityNonSimpleFields,
|
|
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)],
|
|
)
|
|
}
|
|
}
|
|
}
|