Consolidates the in-progress #146 work; valid-instances-rejected reaches 4 on the 2006-11-06 XSTS archive (buckets now 1/262/4/147). Regex: match Unicode scalars rather than grapheme clusters; the empty pattern matches only the empty string; pin the category repertoire to Unicode 3.2, the version the corpus is consistent at, so code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions (the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a 3.2 assignment table derived from DerivedAge, replacing the old Unicode31 files. Content model: assess each child against the particle the content automaton matched, not a by-name lookup (#180). Two same-named particles keep their own fixed values, and an element matching a skip wildcard is no longer validated against its global declaration. Selection is made reproducible when an ambiguous schema slips past the UPA check. XSTS runner: compile and merge every schemaDocument in a schemaTest, not just the first; this also surfaced one previously-masked invalid-instance acceptance (recorded in the baselines). Also folds in attribute-group anyAttribute inheritance, extension wildcard union, namespace-qualified type tables for multi-document xsi:schemaLocation, per-container elementFormDefault, strict and lax attribute wildcard validation, built-in type resolution, and xs:redefine simpleType overrides.
341 lines
18 KiB
Swift
341 lines
18 KiB
Swift
/// File-scope aliases for the XSD complex-type validator, kept out of the
|
|
/// namespace to avoid nesting a type two levels deep.
|
|
typealias XSDFailure = PureXML.Validation.ValidationError
|
|
typealias XSDPath = [PureXML.Validation.PathKey]
|
|
private struct XSDChildValidationFrame {
|
|
let path: XSDPath
|
|
let namespaceBindings: [String: String]
|
|
}
|
|
|
|
private typealias XSDGroup = PureXML.Schema.Group
|
|
private typealias XSDTerm = PureXML.Schema.Term
|
|
private typealias XSDTermLabel = PureXML.Schema.TermLabel
|
|
private typealias XSDProcessContents = PureXML.Schema.ProcessContents
|
|
private typealias XSDElementType = PureXML.Schema.ElementType
|
|
private typealias XSDWildcard = PureXML.Schema.Wildcard
|
|
|
|
public extension PureXML.Schema {
|
|
/// Validates an element against a ``ComplexType``: its attribute uses, its
|
|
/// content model, and, recursively, each child element against the type
|
|
/// declared for its name. Every violation is located by the element's coding
|
|
/// path, and the results are ``PureXML/Validation/ValidationError`` values.
|
|
struct ComplexValidator {
|
|
/// The named type table that `ElementType.typeReference` resolves against.
|
|
let types: [String: ElementType]
|
|
/// Local names of elements declared `nillable="true"`.
|
|
private let nillableElements: Set<String>
|
|
/// The `default`/`fixed` value constraint declared on each element name.
|
|
private let elementConstraints: [String: ValueConstraint]
|
|
/// Local names of complex types declared `abstract="true"`.
|
|
let abstractTypes: Set<String>
|
|
/// Derivation methods each named type forbids through `xsi:type`.
|
|
let typeBlock: [String: Set<DerivationMethod>]
|
|
/// Derivation methods each element declaration forbids through `xsi:type`
|
|
/// (the `block` on the element, distinct from the block on its type).
|
|
let elementBlock: [String: Set<DerivationMethod>]
|
|
/// Each named complex type's base and derivation method.
|
|
let typeDerivation: [String: TypeDerivation]
|
|
/// Global attribute declarations for strict/lax `anyAttribute` validation.
|
|
let globalAttributes: [String: AttributeUse]
|
|
/// Document-scoped xs:ID/xs:IDREF accumulator, filled during the typed walk
|
|
/// and reported by `idErrors()` once the whole tree has been seen. See the
|
|
/// ID/IDREF extension for `idErrors()` and `recordIDs(_:value:at:)`.
|
|
let idTracker = IDTracker()
|
|
|
|
public init(
|
|
types: [String: ElementType] = [:],
|
|
globalAttributes: [String: AttributeUse] = [:],
|
|
nillableElements: Set<String> = [],
|
|
elementConstraints: [String: ValueConstraint] = [:],
|
|
abstractTypes: Set<String> = [],
|
|
typeBlock: [String: Set<DerivationMethod>] = [:],
|
|
elementBlock: [String: Set<DerivationMethod>] = [:],
|
|
typeDerivation: [String: TypeDerivation] = [:],
|
|
) {
|
|
self.types = types
|
|
self.globalAttributes = globalAttributes
|
|
self.nillableElements = nillableElements
|
|
self.elementConstraints = elementConstraints
|
|
self.abstractTypes = abstractTypes
|
|
self.typeBlock = typeBlock
|
|
self.elementBlock = elementBlock
|
|
self.typeDerivation = typeDerivation
|
|
}
|
|
|
|
/// Validates `element` against `type` at `path`, one error per violation.
|
|
public func validate(
|
|
_ element: PureXML.Model.Element,
|
|
against type: ComplexType,
|
|
at path: [PureXML.Validation.PathKey] = [],
|
|
namespaceBindings inheritedBindings: [String: String] = [:],
|
|
) -> [PureXML.Validation.ValidationError] {
|
|
let namespaceBindings = Self.namespaceBindings(for: element, inherited: inheritedBindings)
|
|
var errors: [XSDFailure] = []
|
|
validateAttributes(element, type, at: path, into: &errors)
|
|
// An xsi:nil element keeps its attribute obligations but must have no
|
|
// content; its content model is not otherwise checked.
|
|
if let nilErrors = nilErrors(element, at: path) {
|
|
return errors + nilErrors
|
|
}
|
|
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
errors += elementFixedErrors(element, at: path)
|
|
return errors
|
|
}
|
|
|
|
/// The errors from an `xsi:nil="true"` element: rejecting it when the
|
|
/// element is not nillable, or when it carries content. Returns nil when
|
|
/// the element is not nilled.
|
|
func nilErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure]? {
|
|
guard Self.isNil(element) else { return nil }
|
|
let name = element.name.localName
|
|
if !nillableElements.contains(name) {
|
|
return [XSDFailure(reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
|
|
}
|
|
if Self.hasContent(element) {
|
|
return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)]
|
|
}
|
|
return []
|
|
}
|
|
|
|
/// The error from a `fixed` element value constraint: the element's text
|
|
/// must equal the fixed value in the element type's value space when known.
|
|
func elementFixedErrors(
|
|
_ element: PureXML.Model.Element,
|
|
valueType: SimpleType? = nil,
|
|
particleFixed: String? = nil,
|
|
at path: XSDPath,
|
|
) -> [XSDFailure] {
|
|
guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] }
|
|
let text = Self.rawTextContent(element)
|
|
// An empty element takes the fixed value; only present content must equal it.
|
|
if text.isEmpty { return [] }
|
|
if let valueType, valueType.valueMatches(text, literal: fixed) { return [] }
|
|
return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
|
|
}
|
|
|
|
/// Validates `element` against any element type at `path`, resolving a
|
|
/// `typeReference` through the type table.
|
|
public func validate(
|
|
_ element: PureXML.Model.Element,
|
|
as type: ElementType,
|
|
at path: [PureXML.Validation.PathKey] = [],
|
|
) -> [PureXML.Validation.ValidationError] {
|
|
var errors: [XSDFailure] = []
|
|
let namespaceBindings = Self.namespaceBindings(for: element)
|
|
validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
return errors
|
|
}
|
|
|
|
// MARK: Content
|
|
|
|
private func validateContent(
|
|
_ element: PureXML.Model.Element,
|
|
_ content: ContentType,
|
|
at path: XSDPath,
|
|
namespaceBindings: [String: String],
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
let children = element.children.compactMap(\.element)
|
|
switch content {
|
|
case .empty:
|
|
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
|
|
rejectText(element, at: path, into: &errors)
|
|
case let .simpleContent(type):
|
|
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
|
|
if let error = type.validate(Self.rawTextContent(element)) {
|
|
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
|
}
|
|
case let .elementOnly(particle):
|
|
rejectText(element, at: path, into: &errors)
|
|
validateParticle(particle, children: children, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
case let .mixed(particle):
|
|
validateParticle(particle, children: children, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
}
|
|
}
|
|
|
|
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
guard !Self.textContent(element).isEmpty else { return }
|
|
errors.append(XSDFailure(reason: "element must not contain text", at: path))
|
|
}
|
|
|
|
// MARK: Particles
|
|
|
|
private func validateParticle(
|
|
_ particle: Particle,
|
|
children: [PureXML.Model.Element],
|
|
at path: XSDPath,
|
|
namespaceBindings: [String: String],
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
// Locate each content-model violation at the offending child (or the
|
|
// missing one), with a recovery hint, rather than one opaque failure,
|
|
// then assess every well-placed child against the exact particle the
|
|
// content model matched it to (#180), not a by-name lookup that loses
|
|
// which of two same-named particles, or a wildcard versus a named
|
|
// particle, actually matched.
|
|
let matched: [PureXML.Schema.MatchedParticle?]
|
|
if case let .group(group) = particle.term, group.compositor == .all {
|
|
allStructureErrors(group, children: children, at: path, into: &errors)
|
|
matched = allMatchedParticles(group, children: children)
|
|
} else {
|
|
sequenceStructureErrors(particle, children: children, at: path, into: &errors)
|
|
matched = PureXML.Schema.ContentNFABuilder.build(particle).matchedParticles(children.map(\.name))
|
|
}
|
|
validateChildren(
|
|
children,
|
|
matched: matched,
|
|
frame: XSDChildValidationFrame(path: path, namespaceBindings: namespaceBindings),
|
|
into: &errors,
|
|
)
|
|
}
|
|
|
|
private func validateChildren(
|
|
_ children: [PureXML.Model.Element],
|
|
matched: [PureXML.Schema.MatchedParticle?],
|
|
frame: XSDChildValidationFrame,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
let steps = Self.childSteps(children)
|
|
for (index, child) in children.enumerated() {
|
|
let namespaceBindings = Self.namespaceBindings(for: child, inherited: frame.namespaceBindings)
|
|
let path = frame.path + [steps[index]]
|
|
switch matched[index] {
|
|
case let .element(type, valueConstraint):
|
|
// An element particle without an inlined type (a bare ref)
|
|
// resolves through its global declaration, as before.
|
|
guard let declared = type ?? globalElementDeclaration(for: child.name) else { continue }
|
|
validateChild(child, against: declared, at: path, namespaceBindings: namespaceBindings, particleConstraint: valueConstraint, into: &errors)
|
|
case let .wildcard(wildcard):
|
|
validateWildcardChild(child, processContents: wildcard.processContents, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
case .none:
|
|
// The content model rejected this child (a structure error,
|
|
// already located above); the matched particle is undefined.
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Validates a wildcard-matched child by its `processContents`: skip does
|
|
/// nothing, lax validates against a global declaration when one exists, and
|
|
/// strict requires one.
|
|
private func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
|
|
if let declaration = types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] {
|
|
return declaration
|
|
}
|
|
guard name.namespaceURI == nil else { return nil }
|
|
return types["element:\(name.localName)"]
|
|
}
|
|
|
|
private func validateWildcardChild(
|
|
_ child: PureXML.Model.Element,
|
|
processContents: ProcessContents,
|
|
at path: XSDPath,
|
|
namespaceBindings: [String: String],
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
switch processContents {
|
|
case .skip:
|
|
return
|
|
case .lax:
|
|
if let declaration = globalElementDeclaration(for: child.name) {
|
|
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
}
|
|
case .strict:
|
|
if let declaration = globalElementDeclaration(for: child.name) {
|
|
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
} else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) {
|
|
validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
} else {
|
|
errors.append(XSDFailure(reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The type named by a wildcard-matched element's `xsi:type`, when present.
|
|
private func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> ElementType? {
|
|
guard Self.xsiTypeAttributeValue(child) != nil else { return nil }
|
|
guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
|
|
let type = Self.resolveNamedType(reference, in: types)
|
|
else { return nil }
|
|
return type
|
|
}
|
|
|
|
private func validateChild(
|
|
_ child: PureXML.Model.Element,
|
|
against declared: ElementType,
|
|
at path: XSDPath,
|
|
namespaceBindings: [String: String],
|
|
particleConstraint: ValueConstraint? = nil,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
// An instance `xsi:type` overrides the declared type, provided the named
|
|
// type exists in the schema. Resolve to the type itself rather than a
|
|
// reference so re-entry does not re-read the same xsi:type. A `block` on
|
|
// the declared type can forbid the substitution; an abstract declared
|
|
// type requires one.
|
|
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) {
|
|
errors.append(blocked)
|
|
return
|
|
}
|
|
if case let .typeReference(name) = declared, let missing = abstractTypeError(named: name, child: child, at: path) {
|
|
errors.append(missing)
|
|
return
|
|
}
|
|
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
|
|
switch declared {
|
|
case let .simple(simple):
|
|
// A simple type declares no attribute uses, so a simple-typed
|
|
// element (including one whose simple type comes from an xsi:type
|
|
// override, and including a nilled one) may carry only the
|
|
// schema-instance and namespace attributes; any other is a
|
|
// violation. Checked before the nil short-circuit, since an
|
|
// xsi:nil element is equally bound by it.
|
|
for attribute in child.attributes where !Self.isNamespaceDeclaration(attribute) && !Self.isSchemaInstanceAttribute(attribute) {
|
|
errors.append(XSDFailure(reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'", at: path))
|
|
}
|
|
if let nilErrors = nilErrors(child, at: path) {
|
|
errors += nilErrors
|
|
return
|
|
}
|
|
if !child.children.compactMap(\.element).isEmpty {
|
|
errors.append(XSDFailure(reason: "'\(child.name.localName)' must not have children", at: path))
|
|
}
|
|
// An empty element takes its `default`/`fixed` value; that value
|
|
// (not the empty string) is what must be valid against the type,
|
|
// including any xsi:type override already resolved into `simple`.
|
|
let text = Self.rawTextContent(child)
|
|
let constraint = particleConstraint ?? elementConstraints[child.name.localName]
|
|
let effective = text.isEmpty ? (constraint?.value ?? text) : text
|
|
if let error = simple.validate(effective) {
|
|
errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path))
|
|
}
|
|
recordIDs(simple, value: text, at: path)
|
|
errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path)
|
|
case let .complex(complex):
|
|
errors.append(contentsOf: validate(child, against: complex, at: path, namespaceBindings: namespaceBindings))
|
|
case .typeReference:
|
|
validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
}
|
|
}
|
|
|
|
/// Validates a child against a `typeReference` through the shared resolver,
|
|
/// reporting an unknown name or a circular chain as a located error.
|
|
private func validateResolvedReference(
|
|
_ declared: ElementType,
|
|
_ child: PureXML.Model.Element,
|
|
at path: XSDPath,
|
|
namespaceBindings: [String: String],
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
switch resolveReference(declared) {
|
|
case let .unknown(name):
|
|
errors.append(XSDFailure(reason: "unknown type '\(name)'", at: path))
|
|
case let .circular(name):
|
|
errors.append(XSDFailure(reason: "circular type reference '\(name)'", at: path))
|
|
case let .resolved(resolved):
|
|
validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
|
}
|
|
}
|
|
}
|
|
}
|