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:
parent
7ea1a88a6c
commit
8f2725b62a
@ -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.
|
||||
|
||||
101
Sources/Schema/IdentityFieldSimplicity.swift
Normal file
101
Sources/Schema/IdentityFieldSimplicity.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
62
Sources/Schema/XSDCompiled.swift
Normal file
62
Sources/Schema/XSDCompiled.swift
Normal 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] = [:]
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)])
|
||||
},
|
||||
|
||||
83
Tests/SchemaIdentityFieldSimpleTypeTests.swift
Normal file
83
Tests/SchemaIdentityFieldSimpleTypeTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user