fix(schema): key a local element's block by its effective instance namespace; invalid-instances-accepted 32 to 31 (#147)

The namespaced derivation backbone keyed every element by the container 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 was keyed {targetNS}name while the instance element is {}name, so
blockedSubstitutionError's lookup missed and a blocked xsi:type substitution on it
was accepted (typeDef00801: a local Element with block="extension" did not bar an
extension substitution).

gatherElement now keys a local element by localElementNamespace (target namespace
when qualified, none otherwise); global elements stay keyed by the target
namespace, and the bare-keyed tables are unchanged.

invalid-instances-accepted 32 to 31, no false positive (valid-instances-rejected
and valid-schemas-rejected held at 0). An adversarial review confirmed qualified
local elements are unchanged, global and same-named local element keys stay
distinct, and a non-blocked substitution on a local element is still accepted.
This commit is contained in:
Mihaela Mihaljevic 2026-06-18 16:12:08 +02:00
parent 7e765a3bc1
commit c3393bcaad
4 changed files with 52 additions and 8 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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("""
<xsd:schema xmlns="u" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="u">
<xsd:element name="root"><xsd:complexType><xsd:sequence>
<xsd:element name="Element" type="Type" block="extension"/>
</xsd:sequence></xsd:complexType></xsd:element>
<xsd:complexType name="Type"><xsd:attribute name="value" type="xsd:boolean"/></xsd:complexType>
<xsd:complexType name="derivedType"><xsd:complexContent>
<xsd:extension base="Type"><xsd:attribute name="value1" type="xsd:boolean"/></xsd:extension>
</xsd:complexContent></xsd:complexType>
</xsd:schema>
""")
// Element is unqualified (no elementFormDefault): in instance it is in no namespace.
let doc = #"<test:root xmlns:test="u" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><Element xsi:type="test:derivedType" value="false" value1="true"/></test:root>"#
#expect(try !schema.validate(doc).isEmpty)
}
}

View File

@ -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 {