diff --git a/CHANGELOG.md b/CHANGELOG.md index fc6af39..af25603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- XSTS invalid-instances-accepted (#147): a `block` on a LOCAL element declaration is now keyed by the element's instance (qualified) name in the derivation backbone, so an xsi:type substitution blocked on an unqualified local element is barred. The namespaced backbone keyed every element by the target namespace, but a local element is in the target namespace only when qualified (its own `form`, else the schema's elementFormDefault); an unqualified local element's block went unfound (typeDef00801). invalid-instances-accepted 32 → 31, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. + - XSTS invalid-instances-accepted (#147): an instance `xsi:type` may not name an abstract type. An abstract type cannot be the type of an instance element (cvc-elt.4.3.2), so it is invalid as a substitution even when the declared type would otherwise admit it (typeDef01101); a concrete type is unaffected. This complements the existing check for an abstract DECLARED type with no `xsi:type`. invalid-instances-accepted 34 → 32, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. - XSTS invalid-instances-accepted (#147): an element with a `fixed` value constraint and a mixed content type must carry its fixed character value with no element children (cvc-elt.5.2.2.2.1). The fixed check validated only the concatenated text, so a mixed `fixed` element whose text matched but which also contained child elements (ctZ013) was accepted; element children are now rejected. An empty element (which takes the fixed value) and text equal to the fixed value remain valid. invalid-instances-accepted 37 → 34, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. diff --git a/Sources/Schema/XSDDerivationTables.swift b/Sources/Schema/XSDDerivationTables.swift index e5e86e1..6d753b3 100644 --- a/Sources/Schema/XSDDerivationTables.swift +++ b/Sources/Schema/XSDDerivationTables.swift @@ -81,28 +81,43 @@ extension PureXML.Schema.XSDParser { private static func gatherElement(_ element: XSDTree, in namespace: String?, into tables: inout DerivationTables) { guard let name = XSDDerivNode.attribute(element, "name") else { return } - let key = derivationKey(name, in: namespace) + let isGlobal = element.parent.map { + $0.name?.namespaceURI == xsdNamespace && XSDDerivNode.localName($0) == "schema" + } ?? false + // A global element is always in the target namespace; a local element is in + // it only when qualified (its `form`, else the schema's elementFormDefault), + // so its namespaced key matches the instance element's qualified name. + let elementNamespace = isGlobal ? namespace : localElementNamespace(element, in: namespace) + let key = derivationKey(name, in: elementNamespace) if XSDDerivNode.attribute(element, "abstract") == "true" { tables.abstractElements.insert(name) } let block = blockMethods(of: element, admitting: [.extension, .restriction, .substitution]) if !block.isEmpty { tables.elementBlock[name] = block tables.nsElementBlock[key] = block } - - let isGlobal = element.parent.map { - $0.name?.namespaceURI == xsdNamespace && XSDDerivNode.localName($0) == "schema" - } ?? false if isGlobal { let finalSet = finalMethods(of: element) if !finalSet.isEmpty { tables.elementFinal[name] = finalSet } } - if let type = XSDDerivNode.attribute(element, "type") { tables.elementTypeNames[name] = XSDDerivNode.stripPrefix(type) tables.nsElementTypeNames[key] = resolvedKey(of: type, on: element, default: namespace) } } + /// The namespace a local element's name takes at instance level: the target + /// namespace when the element is qualified (its own `form`, else the schema's + /// `elementFormDefault`), otherwise no namespace. + private static func localElementNamespace(_ element: XSDTree, in namespace: String?) -> String? { + switch XSDDerivNode.attribute(element, "form") { + case "qualified": return namespace + case "unqualified": return nil + default: + let schema = XSDDerivNode.schemaOwner(element) + return XSDDerivNode.attribute(schema, "elementFormDefault") == "qualified" ? namespace : nil + } + } + /// The namespaced (`{ns}local`-based) derivation backbone of a complex type: /// the same `complexContent`/`simpleContent` extension/restriction base as /// ``derivationInfo(_:)``, but with the base resolved to a namespaced key. diff --git a/Tests/SchemaLocalElementBlockKeyTests.swift b/Tests/SchemaLocalElementBlockKeyTests.swift new file mode 100644 index 0000000..2532c04 --- /dev/null +++ b/Tests/SchemaLocalElementBlockKeyTests.swift @@ -0,0 +1,27 @@ +@testable import PureXML +import Testing + +/// A `block` on a LOCAL element declaration applies to that element by its instance +/// (qualified) name. When the schema is unqualified (no elementFormDefault), the +/// local element is in no namespace, so the block must be keyed there for an +/// xsi:type substitution on it to be barred (W3C sun typeDef00801). +@Suite("local element block keying") +struct SchemaLocalElementBlockKeyTests { + @Test("block on an unqualified local element bars an xsi:type extension") + func test_unqualifiedLocalElementBlock() throws { + let schema = try PureXML.Schema.Document(""" + + + + + + + + + + """) + // Element is unqualified (no elementFormDefault): in instance it is in no namespace. + let doc = #""# + #expect(try !schema.validate(doc).isEmpty) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index ccdc350..49594ea 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -150,13 +150,13 @@ struct XSTSSuiteTests { private let knownSchemaValidRejected = 0 private let knownSchemaInvalidAccepted = 69 private let knownInstanceValidRejected = 0 - /// Instance bucket (133 -> 32), per-step deltas in CHANGELOG.md: xsi:type must derive from the + /// Instance bucket (133 -> 31), per-step deltas in CHANGELOG.md: xsi:type must derive from the /// declared type (complex, then atomic and list/union, resolved through the element-ref chain); /// block/blockDefault (incl. "substitution" and an untyped head) bar substitutions after the /// derivation backbone was re-keyed by namespace; ur-type `xsd:anyType` wildcards are `lax`; /// identity fields compare in the decimal value space and take a declared default/fixed when /// absent or empty; a complex type's attribute wildcard is the INTERSECTION of its sources. - private let knownInstanceInvalidAccepted = 32 + private let knownInstanceInvalidAccepted = 31 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws {