From 8f2725b62afebaf921a28617367044eb2be8206f Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Mon, 29 Jun 2026 09:08:25 +0200 Subject: [PATCH] fix(schema): reject an identity-constraint field that selects a non-simple node (#381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 2 + Sources/Schema/IdentityFieldSimplicity.swift | 101 ++++++++++++++++++ Sources/Schema/IdentityValidator.swift | 81 ++++++++------ Sources/Schema/SchemaDocument.swift | 7 ++ Sources/Schema/XSDCompiled.swift | 62 +++++++++++ Sources/Schema/XSDContext.swift | 58 ---------- Sources/Schema/XSDParser.swift | 12 +-- Sources/Schema/XSDParserHelpers.swift | 2 +- Sources/Schema/XSDValidations.swift | 4 + .../SchemaIdentityFieldSimpleTypeTests.swift | 83 ++++++++++++++ Tests/XSTSSuiteTests.swift | 5 +- 11 files changed, 319 insertions(+), 98 deletions(-) create mode 100644 Sources/Schema/IdentityFieldSimplicity.swift create mode 100644 Sources/Schema/XSDCompiled.swift create mode 100644 Tests/SchemaIdentityFieldSimpleTypeTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 833495b..2842829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Schema/IdentityFieldSimplicity.swift b/Sources/Schema/IdentityFieldSimplicity.swift new file mode 100644 index 0000000..4486286 --- /dev/null +++ b/Sources/Schema/IdentityFieldSimplicity.swift @@ -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 { + var keys: Set = [] + 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 + } +} diff --git a/Sources/Schema/IdentityValidator.swift b/Sources/Schema/IdentityValidator.swift index d7bb9c2..e37a392 100644 --- a/Sources/Schema/IdentityValidator.swift +++ b/Sources/Schema/IdentityValidator.swift @@ -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 /// 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 = [], 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 { diff --git a/Sources/Schema/SchemaDocument.swift b/Sources/Schema/SchemaDocument.swift index 9f3997c..f75fa89 100644 --- a/Sources/Schema/SchemaDocument.swift +++ b/Sources/Schema/SchemaDocument.swift @@ -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 private let nillableElements: Set private let elementConstraints: [String: ValueConstraint] private let abstractTypes: Set @@ -67,6 +68,7 @@ public extension PureXML.Schema { constraints: [String: [IdentityConstraint]], identityFieldTypes: [String: SimpleType] = [:], identityFieldConstraints: [String: ValueConstraint] = [:], + identityNonSimpleFields: Set = [], nillableElements: Set, elementConstraints: [String: ValueConstraint], abstractTypes: Set, @@ -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) } diff --git a/Sources/Schema/XSDCompiled.swift b/Sources/Schema/XSDCompiled.swift new file mode 100644 index 0000000..3aad324 --- /dev/null +++ b/Sources/Schema/XSDCompiled.swift @@ -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 = [] + /// Local names of elements declared `nillable="true"`. + var nillableElements: Set = [] + /// 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 = [] + /// Local names of element declarations declared `abstract="true"`: they may + /// not appear in an instance directly, only through a substitution member. + var abstractElements: Set = [] + /// Derivation methods the named type forbids when used through `xsi:type`. + /// Bare-keyed. + var typeBlock: [String: Set] = [:] + /// Derivation methods each element declaration forbids through `xsi:type`. + /// Bare-keyed. + var elementBlock: [String: Set] = [:] + /// 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 = [] + var nsTypeBlock: [String: Set] = [:] + var nsElementBlock: [String: Set] = [:] + 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] = [:] + /// 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] = [:] + } +} diff --git a/Sources/Schema/XSDContext.swift b/Sources/Schema/XSDContext.swift index 4246a3d..a32e271 100644 --- a/Sources/Schema/XSDContext.swift +++ b/Sources/Schema/XSDContext.swift @@ -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 = [] - /// 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 = [] - /// Local names of element declarations declared `abstract="true"`: they may - /// not appear in an instance directly, only through a substitution member. - var abstractElements: Set = [] - /// Derivation methods the named type forbids when used through `xsi:type`. - /// Bare-keyed. - var typeBlock: [String: Set] = [:] - /// Derivation methods each element declaration forbids through `xsi:type`. - /// Bare-keyed. - var elementBlock: [String: Set] = [:] - /// 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 = [] - var nsTypeBlock: [String: Set] = [:] - var nsElementBlock: [String: Set] = [:] - 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] = [:] - /// 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 diff --git a/Sources/Schema/XSDParser.swift b/Sources/Schema/XSDParser.swift index cace1cb..1878ad0 100644 --- a/Sources/Schema/XSDParser.swift +++ b/Sources/Schema/XSDParser.swift @@ -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 } 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, diff --git a/Sources/Schema/XSDParserHelpers.swift b/Sources/Schema/XSDParserHelpers.swift index 1d6bc0d..c03aa4a 100644 --- a/Sources/Schema/XSDParserHelpers.swift +++ b/Sources/Schema/XSDParserHelpers.swift @@ -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 diff --git a/Sources/Schema/XSDValidations.swift b/Sources/Schema/XSDValidations.swift index aec0f16..11d8556 100644 --- a/Sources/Schema/XSDValidations.swift +++ b/Sources/Schema/XSDValidations.swift @@ -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 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 = [], ) { 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)]) }, diff --git a/Tests/SchemaIdentityFieldSimpleTypeTests.swift b/Tests/SchemaIdentityFieldSimpleTypeTests.swift new file mode 100644 index 0000000..1a42b9d --- /dev/null +++ b/Tests/SchemaIdentityFieldSimpleTypeTests.swift @@ -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(""" + + + + + + + + + + + + + + + + + + + + """) + } + + @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("1") + #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(""" + + + + + + + + + + + + + + + + + """) + #expect(try schema.validate("12").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 = """ + \ + 1 + """ + #expect(try schema.validate(xml).isEmpty) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index f1f9060..9e4bf28 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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