fix(schema): reject an identity-constraint field that selects a non-simple node (#381)

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.
This commit is contained in:
Mihaela Mihaljevic 2026-06-29 09:08:25 +02:00 committed by GitHub
parent 7ea1a88a6c
commit 8f2725b62a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 319 additions and 98 deletions

View File

@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- An XSD `key`/`keyref`/`unique` field that selects a node whose declared type is not simple is now rejected (cvc-identity-constraint.3), rather than wrongly accepted. The XSD identity-constraint rule requires each 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 failing W3C XSTS case idZ010 nests `anyType` fields several levels deep). A compile-time pass (`XSDParser.identityNonSimpleFieldKeys`) now 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` (cvc reads the node's governing type, so a text-only `anyType` element with `xsi:type="xsd:int"` is still 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. 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 introduced), and the full suite passes.
- The XSD regex escapes `\i` and `\c` now use the XML 1.0 Second Edition name classes (Appendix B), not the parser's Fifth Edition Name production. XSD 1.0 (Datatypes Appendix F) defines `\i` as the characters that may begin an XML Name (`Letter | '_' | ':'`) and `\c` as the characters that may appear in one (`NameChar`), citing the XML 1.0 Second Edition productions, which are the enumerated Unicode-2.0-era character classes of XML 1.0 Appendix B. `\i`/`\c` were wired to `PureXML.Parsing.XMLCharacter`, which correctly implements the Fifth Edition production used to parse live XML, but the two genuinely differ: the Fifth Edition admits whole blocks where the Second Edition has gaps, so `\c` over-accepted, for example the combining mark `U+0346` (`COMBINING BRIDGE ABOVE`), which Appendix B's `CombiningChar` excludes (its first range ends at `U+0345`). A new `XSDNameCharacter`, transcribed clean-room from the Appendix B grammar (identical across editions one through four) and stored as sorted code-point ranges with binary-search membership, now backs both escapes; the parser's own name check is untouched. The two range tables are verified to reproduce the W3C XSTS `\i`/`\c` enumerations (`reZ005`, `reZ006`, 34516 and 35122 code points) exactly and in both directions, so there is no false-positive surface. Verified over the full W3C XSTS corpus in release: `invalid-instances-accepted` 12 -> 11 (`reZ006i`, whose value is `U+0346`, now rejects), with both false-positive buckets at 0. Found by triaging the invalid-instance tail to a single boundary code point (`U+0345` valid vs `U+0346` invalid) and reading the XML 1.0 Appendix B grammar.
- The Ethiopic digits `U+1369`..`U+1371` (`ETHIOPIC DIGIT ONE`..`NINE`) now match `\d` in an XSD pattern and not `\D`. XSD 1.0 fixes the regular-expression repertoire at Unicode 3.2 (Datatypes Appendix F), where these nine code points are general category `Nd` and each carries a decimal value; a later Unicode reclassified them to `No`. `XSDCategory` derives `\d` (`== \p{Nd}`) from the running platform's category, pinned to 3.2 by an assigned-in-3.2 gate plus a per-code-point override table, but the override table was missing these nine, so `\d` used the modern `No` and rejected them. This was a two-directional bug: a valid instance whose `\d`-constrained value was an Ethiopic digit was wrongly rejected (the cardinal rule: never reject a valid value), and a `\D`-constrained value of one was wrongly accepted. They are now pinned to `Nd`, which is the complete set of code points that were `Nd` at 3.2 and are not today (the reverse direction is empty: every code point that became `Nd` later was unassigned at 3.2 and so already reports `Cn`, verified by diffing the 3.2 and current Unicode `Nd` sets), so `\w` is unaffected (an Ethiopic numeral is category `N` either way, neither punctuation, separator, nor other). Verified over the full W3C XSTS corpus in release: `invalid-instances-accepted` 14 -> 12 (`reT17`, `reT38`, the `\D` cases, now reject), the matching `\d` valid cases (`reS17`, `reS38`, `reZ004`) no longer need their suspect-test exclusion and pass as ordinary agreements, and the two false-positive buckets stay 0. Found while triaging the invalid-instance tail against libxml2 (which rejects `reT17`/`reT38`, the reference-aligned reading) back to the authoritative `UnicodeData-3.2.0`; a prior note that had treated these as a disputed spec-divergence was the wrong call and is corrected.

View File

@ -0,0 +1,101 @@
extension PureXML.Schema.XSDParser {
/// Keys of identity-constraint fields whose target element is, by declaration,
/// not of simple type: the ur-type `anyType` from an absent type, or an inline
/// complex type without simple content. XSD 1.0 §3.11.4 (cvc-identity-constraint.3)
/// requires a field to identify a node of simple type, so such a field makes the
/// instance invalid (XSTS idZ010). Resolved conservatively: a key is recorded only
/// when the selector and field paths resolve unambiguously, by element-name steps,
/// to a declared element that is provably non-simple. Any wildcard, descendant
/// axis, predicate, named (unresolved) type, or `ref` leaves the key out, so a
/// valid field, which always names a simple-typed element or attribute, is never
/// flagged.
static func identityNonSimpleFieldKeys(_ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> Set<String> {
var keys: Set<String> = []
for container in containers {
for element in descendants(container, named: "element") {
let constraints = PureXML.Schema.XSDNode.elementChildren(element).compactMap(constraint)
for constraint in constraints {
guard let selectorTarget = pathTargetElement(from: element, path: constraint.selector) else { continue }
for field in constraint.fields where !field.hasPrefix("@") {
guard let fieldTarget = pathTargetElement(from: selectorTarget, path: field),
elementIsDeclaredNonSimple(fieldTarget, context)
else { continue }
keys.insert(identityFieldKey(constraint: constraint, field: field))
}
}
}
}
return keys
}
/// The declared element node a selector or field XPath reaches from `start`,
/// following only the restricted-XPath child-name steps (a leading or interior
/// `.` is self). Returns nil for any step that is not a plain name test (a
/// wildcard `*`, a `//` descendant step, an axis `::`, a predicate `[`, an
/// attribute `@`), or that names no direct child element, so the caller flags
/// only paths it has resolved with certainty.
private static func pathTargetElement(from start: XSDTree, path: String) -> XSDTree? {
var current: XSDTree? = start
for rawStep in path.split(separator: "/", omittingEmptySubsequences: false) {
let step = String(rawStep).trimmingXMLWhitespace()
if step == "." { continue }
guard isSimpleNameTest(step), let node = current,
let child = directChildElement(named: PureXML.Schema.XSDNode.stripPrefix(step), of: node)
else { return nil }
current = child
}
return current
}
/// Whether `step` is a plain element name test (`NCName` or `prefix:NCName`),
/// not a wildcard, axis, predicate, or `//` (an empty step from a split).
private static func isSimpleNameTest(_ step: String) -> Bool {
guard !step.isEmpty, !step.contains(where: { "*[]()|@ \t".contains($0) }) else { return false }
return step.count(where: { $0 == ":" }) <= 1
}
/// The direct child element declaration of `element`'s `complexType` model with
/// the given local name, descending through model groups but not into another
/// element's own type, and never matching a `ref` (which has no `name`).
private static func directChildElement(named local: String, of element: XSDTree) -> XSDTree? {
guard let complexType = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") else { return nil }
return modelChildElements(of: complexType).first { PureXML.Schema.XSDNode.attribute($0, "name") == local }
}
/// Every direct element declaration in a complex type's content model, reached
/// through the model-group and derivation wrappers but stopping at each
/// `element` (so a nested element's own children are not collected).
private static func modelChildElements(of node: XSDTree) -> [XSDTree] {
var result: [XSDTree] = []
for child in PureXML.Schema.XSDNode.elementChildren(node) {
switch PureXML.Schema.XSDNode.localName(child) {
case "element": result.append(child)
case "sequence", "choice", "all", "group", "complexContent", "restriction", "extension":
result += modelChildElements(of: child)
default: break
}
}
return result
}
/// Whether `element`'s declared type is provably not a simple type: an explicit
/// `xsd:anyType`, an inline `complexType` without `simpleContent`, or no type at
/// all (the ur-type `anyType`). A builtin, `anySimpleType`, an inline
/// `simpleType`, `simpleContent`, or any named non-builtin type is treated as
/// possibly-simple and not flagged, so the check never rejects a valid field.
private static func elementIsDeclaredNonSimple(_ element: XSDTree, _ context: PureXML.Schema.XSDContext) -> Bool {
if let typeName = PureXML.Schema.XSDNode.attribute(element, "type") {
let bindings = PureXML.Schema.XSDParser.namespaceBindingsInScope(of: element, defaultBindings: context.namespaceBindings)
let local = PureXML.Schema.XSDNode.stripPrefix(typeName)
guard PureXML.Schema.XSDNode.referenceNamespace(typeName, bindings) == PureXML.Schema.XSDParser.xsdNamespace else {
return false
}
return local == "anyType"
}
if PureXML.Schema.XSDNode.firstChild(element, named: "simpleType") != nil { return false }
if let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") {
return PureXML.Schema.XSDNode.firstChild(complex, named: "simpleContent") == nil
}
return true
}
}

View File

@ -16,6 +16,10 @@ extension PureXML.Schema {
/// absent attribute or an empty element whose declaration carries one of
/// these, the field's identity value is that value, not nothing.
let fieldConstraints: [String: PureXML.Schema.ValueConstraint]
/// Keys (``XSDParser/identityFieldKey(constraint:field:)``) of fields whose
/// declared target is not a simple type, computed at compile time, so the
/// field violates cvc-identity-constraint.3 (XSTS idZ010).
let nonSimpleFields: Set<String>
/// Global element and type declarations for runtime field typing.
let types: [String: ElementType]
/// Selector and field XPaths compiled once per run, not per element.
@ -31,11 +35,13 @@ extension PureXML.Schema {
constraints: [String: [IdentityConstraint]],
fieldTypes: [String: SimpleType] = [:],
fieldConstraints: [String: PureXML.Schema.ValueConstraint] = [:],
nonSimpleFields: Set<String> = [],
types: [String: ElementType] = [:],
) {
self.constraints = constraints
self.fieldTypes = fieldTypes
self.fieldConstraints = fieldConstraints
self.nonSimpleFields = nonSimpleFields
self.types = types
}
@ -217,7 +223,18 @@ extension PureXML.Schema {
// simple-content element, a text-only node, and an attribute are left alone.
let element = selected.first.flatMap { $0.kind == .element ? $0 : nil }
let hasElementChildren = element?.children.contains { $0.kind == .element } ?? false
if element != nil, hasElementChildren || fieldTargetIsComplexNonSimple(field, at: target) {
// A field whose DECLARED target is non-simple (the ur-type anyType or
// an inline complex type without simple content, resolved at compile
// time, XSTS idZ010) is invalid unless the instance gives the node a
// simple type through xsi:type: cvc-identity-constraint.3 reads the
// node's governing type, so a text-only anyType element validates only
// when xsi:type names a simple type. With an xsi:type present the
// runtime value comparison already governs, so the field is left.
let declaredNonSimple = element.map { node in
nonSimpleFields.contains(PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field))
&& Self.xsiTypeLocalName(of: node) == nil
} ?? false
if element != nil, hasElementChildren || declaredNonSimple || fieldTargetIsComplexNonSimple(field, at: target) {
return .init(
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must identify a node with simple content",
at: targetPath + step,
@ -300,41 +317,41 @@ extension PureXML.Schema {
}
return fieldTypes[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)]
}
private func simpleType(of declaration: ElementType) -> SimpleType? {
switch declaration {
case let .simple(simple): simple
case let .complex(complex):
if case let .simpleContent(simple) = complex.content { simple } else { nil }
case .typeReference: nil
}
}
private static func effectiveType(of value: PureXML.XPath.Value) -> SimpleType? {
guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first,
treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode),
let builtin = BuiltinType(rawValue: local)
else { return nil }
return SimpleType(base: builtin)
}
/// The local name of a tree node's `xsi:type` attribute value, recognized by
/// the schema-instance namespace or the conventional `xsi` prefix, or nil.
private static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? {
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
guard let attribute = node.attributes.first(where: {
$0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi")
}) else { return nil }
return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value
}
private func label(_ constraint: IdentityConstraint) -> String {
constraint.kind == .key ? "key" : "unique"
}
}
}
extension PureXML.Schema.IdentityValidator {
func simpleType(of declaration: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? {
switch declaration {
case let .simple(simple): simple
case let .complex(complex):
if case let .simpleContent(simple) = complex.content { simple } else { nil }
case .typeReference: nil
}
}
static func effectiveType(of value: PureXML.XPath.Value) -> PureXML.Schema.SimpleType? {
guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first,
treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode),
let builtin = PureXML.Schema.BuiltinType(rawValue: local)
else { return nil }
return PureXML.Schema.SimpleType(base: builtin)
}
/// The local name of a tree node's `xsi:type` attribute value, recognized by
/// the schema-instance namespace or the conventional `xsi` prefix, or nil.
static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? {
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
guard let attribute = node.attributes.first(where: {
$0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi")
}) else { return nil }
return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value
}
func label(_ constraint: PureXML.Schema.IdentityConstraint) -> String {
constraint.kind == .key ? "key" : "unique"
}
/// Whether a constraint collects tuples (`key`/`unique`) rather than
/// referencing them (`keyref`).
func isNonRef(_ constraint: PureXML.Schema.IdentityConstraint) -> Bool {

View File

@ -47,6 +47,7 @@ public extension PureXML.Schema {
private let constraints: [String: [IdentityConstraint]]
private let identityFieldTypes: [String: SimpleType]
private let identityFieldConstraints: [String: ValueConstraint]
private let identityNonSimpleFields: Set<String>
private let nillableElements: Set<String>
private let elementConstraints: [String: ValueConstraint]
private let abstractTypes: Set<String>
@ -67,6 +68,7 @@ public extension PureXML.Schema {
constraints: [String: [IdentityConstraint]],
identityFieldTypes: [String: SimpleType] = [:],
identityFieldConstraints: [String: ValueConstraint] = [:],
identityNonSimpleFields: Set<String> = [],
nillableElements: Set<String>,
elementConstraints: [String: ValueConstraint],
abstractTypes: Set<String>,
@ -83,6 +85,7 @@ public extension PureXML.Schema {
self.constraints = constraints
self.identityFieldTypes = identityFieldTypes
self.identityFieldConstraints = identityFieldConstraints
self.identityNonSimpleFields = identityNonSimpleFields
self.nillableElements = nillableElements
self.elementConstraints = elementConstraints
self.abstractTypes = abstractTypes
@ -113,6 +116,7 @@ public extension PureXML.Schema {
constraints: mineFirst(constraints, other.constraints),
identityFieldTypes: mineFirst(identityFieldTypes, other.identityFieldTypes),
identityFieldConstraints: mineFirst(identityFieldConstraints, other.identityFieldConstraints),
identityNonSimpleFields: identityNonSimpleFields.union(other.identityNonSimpleFields),
nillableElements: nillableElements.union(other.nillableElements),
elementConstraints: mineFirst(elementConstraints, other.elementConstraints),
abstractTypes: abstractTypes.union(other.abstractTypes),
@ -136,6 +140,7 @@ public extension PureXML.Schema {
types = compiled.types
constraints = compiled.constraints
identityFieldTypes = compiled.identityFieldTypes
identityNonSimpleFields = compiled.identityNonSimpleFields
identityFieldConstraints = compiled.identityFieldConstraints
nillableElements = compiled.nillableElements
elementConstraints = compiled.elementConstraints
@ -262,6 +267,7 @@ public extension PureXML.Schema {
constraints: constraints,
fieldTypes: identityFieldTypes,
fieldConstraints: identityFieldConstraints,
nonSimpleFields: identityNonSimpleFields,
types: types,
)
.validate(PureXML.Model.TreeNode(.element(root)), at: [.element(root.name.description)])
@ -302,6 +308,7 @@ public extension PureXML.Schema {
globalAttributes: globalAttributes,
identityFieldTypes: identityFieldTypes,
identityFieldConstraints: identityFieldConstraints,
identityNonSimpleFields: identityNonSimpleFields,
)
return PureXML.Validation.XSD.validator().errors(for: .element(root), in: context)
}

View File

@ -0,0 +1,62 @@
extension PureXML.Schema {
/// The result of compiling a schema document: its global element
/// declarations, its named-type table, and the identity constraints declared
/// on each element name.
struct XSDCompiled {
var elements: [String: ElementType]
var types: [String: ElementType]
var constraints: [String: [IdentityConstraint]]
/// Simple types for identity-constraint field paths.
var identityFieldTypes: [String: SimpleType] = [:]
/// `default`/`fixed` value constraints on identity-constraint field
/// targets, so an absent attribute or empty element takes that value as its
/// identity-tuple component.
var identityFieldConstraints: [String: ValueConstraint] = [:]
/// Identity fields whose declared target is not a simple type, so the field
/// violates cvc-identity-constraint.3 (idZ010).
var identityNonSimpleFields: Set<String> = []
/// Local names of elements declared `nillable="true"`.
var nillableElements: Set<String> = []
/// The `default`/`fixed` value constraint declared on each element name.
var elementConstraints: [String: ValueConstraint] = [:]
/// Local names of complex types declared `abstract="true"`. Bare-keyed, for
/// the schema-consistency rules and direct unit tests.
var abstractTypes: Set<String> = []
/// Local names of element declarations declared `abstract="true"`: they may
/// not appear in an instance directly, only through a substitution member.
var abstractElements: Set<String> = []
/// Derivation methods the named type forbids when used through `xsi:type`.
/// Bare-keyed.
var typeBlock: [String: Set<DerivationMethod>] = [:]
/// Derivation methods each element declaration forbids through `xsi:type`.
/// Bare-keyed.
var elementBlock: [String: Set<DerivationMethod>] = [:]
/// Each named complex type's base type and derivation method, the backbone
/// the schema-consistency rules walk. Bare-keyed.
var typeDerivation: [String: TypeDerivation] = [:]
/// Namespaced (`{ns}local`) views of the abstract-type set, `block` tables,
/// and derivation backbone, for the INSTANCE-validity subsystem, where two
/// imported types sharing a local name in different namespaces must not
/// collide.
var nsAbstractTypes: Set<String> = []
var nsTypeBlock: [String: Set<DerivationMethod>] = [:]
var nsElementBlock: [String: Set<DerivationMethod>] = [:]
var nsTypeDerivation: [String: TypeDerivation] = [:]
/// Each named union simple type's member type keys (`{ns}local`), so the
/// `xsi:type`-`block` check can follow a derivation that reaches the
/// declared union through one of its members (cos-st-derived-OK 2.2.4).
var nsUnionMembers: [String: [String]] = [:]
/// Derivation methods each named type declares `final`, for the
/// schema-consistency rules.
var typeFinal: [String: Set<DerivationMethod>] = [:]
/// The schema's target namespace, so the root element's namespace can be
/// checked against it.
var targetNamespace: String?
/// Schema-validity findings gathered while parsing (malformed facet
/// definitions and the like), reported together with the model-level
/// consistency findings when the schema is compiled.
var schemaErrors: [PureXML.Validation.ValidationError] = []
/// Global attribute declarations keyed by ``attributeDeclarationKey(_:)``.
var globalAttributes: [String: AttributeUse] = [:]
}
}

View File

@ -182,64 +182,6 @@ extension PureXML.Schema {
}
}
/// The result of compiling a schema document: its global element
/// declarations, its named-type table, and the identity constraints declared
/// on each element name.
struct XSDCompiled {
var elements: [String: ElementType]
var types: [String: ElementType]
var constraints: [String: [IdentityConstraint]]
/// Simple types for identity-constraint field paths.
var identityFieldTypes: [String: SimpleType] = [:]
/// `default`/`fixed` value constraints on identity-constraint field
/// targets, so an absent attribute or empty element takes that value as its
/// identity-tuple component.
var identityFieldConstraints: [String: ValueConstraint] = [:]
/// Local names of elements declared `nillable="true"`.
var nillableElements: Set<String> = []
/// The `default`/`fixed` value constraint declared on each element name.
var elementConstraints: [String: ValueConstraint] = [:]
/// Local names of complex types declared `abstract="true"`. Bare-keyed, for
/// the schema-consistency rules and direct unit tests.
var abstractTypes: Set<String> = []
/// Local names of element declarations declared `abstract="true"`: they may
/// not appear in an instance directly, only through a substitution member.
var abstractElements: Set<String> = []
/// Derivation methods the named type forbids when used through `xsi:type`.
/// Bare-keyed.
var typeBlock: [String: Set<DerivationMethod>] = [:]
/// Derivation methods each element declaration forbids through `xsi:type`.
/// Bare-keyed.
var elementBlock: [String: Set<DerivationMethod>] = [:]
/// Each named complex type's base type and derivation method, the backbone
/// the schema-consistency rules walk. Bare-keyed.
var typeDerivation: [String: TypeDerivation] = [:]
/// Namespaced (`{ns}local`) views of the abstract-type set, `block` tables,
/// and derivation backbone, for the INSTANCE-validity subsystem, where two
/// imported types sharing a local name in different namespaces must not
/// collide.
var nsAbstractTypes: Set<String> = []
var nsTypeBlock: [String: Set<DerivationMethod>] = [:]
var nsElementBlock: [String: Set<DerivationMethod>] = [:]
var nsTypeDerivation: [String: TypeDerivation] = [:]
/// Each named union simple type's member type keys (`{ns}local`), so the
/// `xsi:type`-`block` check can follow a derivation that reaches the
/// declared union through one of its members (cos-st-derived-OK 2.2.4).
var nsUnionMembers: [String: [String]] = [:]
/// Derivation methods each named type declares `final`, for the
/// schema-consistency rules.
var typeFinal: [String: Set<DerivationMethod>] = [:]
/// The schema's target namespace, so the root element's namespace can be
/// checked against it.
var targetNamespace: String?
/// Schema-validity findings gathered while parsing (malformed facet
/// definitions and the like), reported together with the model-level
/// consistency findings when the schema is compiled.
var schemaErrors: [PureXML.Validation.ValidationError] = []
/// Global attribute declarations keyed by ``attributeDeclarationKey(_:)``.
var globalAttributes: [String: AttributeUse] = [:]
}
/// Collects the located findings of simple-type facet and pattern validity,
/// which are computed during type compilation (they need the resolved base
/// type) and surfaced by the `simpleTypeFacetsAreValid` validation rather than

View File

@ -16,12 +16,9 @@ private typealias XSDCompiled = PureXML.Schema.XSDCompiled
extension PureXML.Schema {
/// Parses an XSD schema document into its global element declarations and its
/// named-type table. Matches the schema vocabulary by local name, so the XML
/// Schema namespace prefix may be anything. Supports global and local element
/// declarations, named and inline simple and complex types, `sequence`,
/// `choice`, and `all` model groups with occurrence, attribute uses, simple
/// content, the full facet set, `list` and `union` simple types, named
/// attribute groups and model groups, and element and group references.
/// named-type table, matching the schema vocabulary by local name (so the XML
/// Schema namespace prefix may be anything). Covers elements, simple and complex
/// types, model groups, attribute uses, facets, `list`/`union`, and references.
enum XSDParser {
static func parse(_ xsd: String, loader: (String) -> String? = { _ in nil }) throws -> XSDCompiled {
let root = try PureXML.parseTree(xsd)
@ -155,6 +152,7 @@ extension PureXML.Schema.XSDParser {
var context: XSDContext
var identityFieldTypes: [String: SimpleType]
var identityFieldConstraints: [String: ValueConstraint]
var identityNonSimpleFields: Set<String>
}
static func finishCompile(
@ -190,6 +188,7 @@ extension PureXML.Schema.XSDParser {
context: context,
identityFieldTypes: identityFieldTypes(containers, context),
identityFieldConstraints: identityFieldConstraints(containers, context),
identityNonSimpleFields: identityNonSimpleFieldKeys(containers, context),
))
}
@ -201,6 +200,7 @@ extension PureXML.Schema.XSDParser {
constraints: identityConstraints(build.containers),
identityFieldTypes: build.identityFieldTypes,
identityFieldConstraints: build.identityFieldConstraints,
identityNonSimpleFields: build.identityNonSimpleFields,
nillableElements: nillable,
elementConstraints: elementConstraints,
abstractTypes: build.derivation.abstractTypes,

View File

@ -266,7 +266,7 @@ extension PureXML.Schema.XSDParser {
return nil
}
private static func constraint(_ node: XSDTree) -> PureXML.Schema.IdentityConstraint? {
static func constraint(_ node: XSDTree) -> PureXML.Schema.IdentityConstraint? {
let kind: PureXML.Schema.IdentityConstraintKind
switch PureXML.Schema.XSDNode.localName(node) {
case "unique": kind = .unique

View File

@ -16,6 +16,7 @@ public extension PureXML.Validation {
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],
@ -31,6 +32,7 @@ public extension PureXML.Validation {
globalAttributes: [String: PureXML.Schema.AttributeUse] = [:],
identityFieldTypes: [String: PureXML.Schema.SimpleType] = [:],
identityFieldConstraints: [String: PureXML.Schema.ValueConstraint] = [:],
identityNonSimpleFields: Set<String> = [],
) {
self.types = types
self.constraints = constraints
@ -45,6 +47,7 @@ public extension PureXML.Validation {
self.globalAttributes = globalAttributes
self.identityFieldTypes = identityFieldTypes
self.identityFieldConstraints = identityFieldConstraints
self.identityNonSimpleFields = identityNonSimpleFields
}
}
}
@ -108,6 +111,7 @@ public extension PureXML.Validation {
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)])
},

View File

@ -0,0 +1,83 @@
import Testing
@testable import PureXML
/// cvc-identity-constraint.3: a `key`/`keyref`/`unique` field must identify a node
/// of simple type. A field that reaches an element whose declared type is the
/// ur-type `anyType` (no type) or a complex type without simple content is invalid,
/// unless the instance gives the node a simple type through `xsi:type`. Mirrors the
/// W3C XSTS idZ010 shape (a `key`/`keyref` whose fields select no-type elements
/// nested through a path), the case PureXML previously accepted.
@Suite("Identity-constraint field must select a simple-typed node")
struct SchemaIdentityFieldSimpleTypeTests {
/// idZ010 in miniature: `root/a` carries a `key` whose field path reaches `c`,
/// declared with no type (so `anyType`). The field is not simple, so the
/// instance is invalid even though the value would otherwise be a fine key.
private func anyTypeFieldSchema() throws -> PureXML.Schema.Document {
try PureXML.Schema.Document("""
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="root">
<xsd:complexType><xsd:sequence>
<xsd:element name="a" maxOccurs="unbounded">
<xsd:complexType><xsd:sequence>
<xsd:element name="b">
<xsd:complexType><xsd:sequence>
<xsd:element name="c"/>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
<xsd:key name="k">
<xsd:selector xpath="a"/>
<xsd:field xpath="./b/c"/>
</xsd:key>
</xsd:element>
</xsd:schema>
""")
}
@Test("a key field reaching a no-type (anyType) element is rejected")
func test_anyTypeFieldRejected() throws {
let schema = try anyTypeFieldSchema()
let errors = try schema.validate("<root><a><b><c>1</c></b></a></root>")
#expect(!errors.isEmpty)
#expect(errors.contains { $0.reason.contains("simple content") })
}
@Test("a simple-typed field at the same path is accepted")
func test_simpleTypedFieldAccepted() throws {
let schema = try PureXML.Schema.Document("""
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="root">
<xsd:complexType><xsd:sequence>
<xsd:element name="a" maxOccurs="unbounded">
<xsd:complexType><xsd:sequence>
<xsd:element name="b">
<xsd:complexType><xsd:sequence>
<xsd:element name="c" type="xsd:int"/>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
</xsd:element>
</xsd:sequence></xsd:complexType>
<xsd:key name="k"><xsd:selector xpath="a"/><xsd:field xpath="./b/c"/></xsd:key>
</xsd:element>
</xsd:schema>
""")
#expect(try schema.validate("<root><a><b><c>1</c></b></a><a><b><c>2</c></b></a></root>").isEmpty)
}
@Test("an anyType field rescued by a simple xsi:type is accepted")
func test_anyTypeRescuedByXsiType() throws {
// The element is declared anyType, but the instance gives it xsi:type=int,
// so its governing type is simple and the field is valid (cvc reads the
// governing type). This is the guard that keeps the check from a false
// positive on a legitimately simple-at-runtime node.
let schema = try anyTypeFieldSchema()
let xml = """
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\
<a><b><c xsi:type="xsd:int">1</c></b></a></root>
"""
#expect(try schema.validate(xml).isEmpty)
}
}

View File

@ -86,7 +86,10 @@ struct XSTSSuiteTests {
/// documentation; under that prescribed behavior both instances are valid.
/// (schA7.i, which looks similar, is NOT excluded: its `invalid` is spec-grounded,
/// a genuine policy-dependent deviation tracked in docs/xsts-deviations.md.)
private let knownInstanceInvalidAccepted = 8
/// 8 -> 7: an identity-constraint field that selects a node whose declared type is
/// not simple (the ur-type anyType, no xsi:type rescue) is rejected per
/// cvc-identity-constraint.3 (idZ010). See ``XSDParser/identityNonSimpleFieldKeys``.
private let knownInstanceInvalidAccepted = 7
/// Instance tests excluded from the counts because their expected validity is not
/// authoritative, mirroring how the suite's own `status="queried"` entries are
/// skipped. Each must carry a spec-grounded justification; this is never a place to