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:
parent
7e765a3bc1
commit
c3393bcaad
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
27
Tests/SchemaLocalElementBlockKeyTests.swift
Normal file
27
Tests/SchemaLocalElementBlockKeyTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user