fix(schema): reject references into never-imported namespaces
On the cross-document skip path (a schema declares import/include/redefine but no external document was loaded), the reference check stood down whole, so a prefixed reference whose namespace is never imported was accepted even though it can resolve to no component under any loading (corpus schZ011's d:d, where namespaces a/b/c are imported but d is not). Add undeclaredNamespaceReferenceErrors, run only on that skip path, to flag a reference whose namespace is neither the target, the XSD or XML namespace, nor named by any xs:import. To stay sound when the import closure is unknown, it stands down entirely the moment any external carries a schemaLocation: that document may transitively import the referenced namespace (corpus schZ004 imports only a, yet a's document imports b, so b:b resolves). It applies only when every external is location-less and the closure is provably complete. XSTS invalid-schemas-accepted 43 -> 39 (schZ011_a-d); valid-schemas-rejected held at 0; no other bucket moved.
This commit is contained in:
parent
2a9a5afaa9
commit
7256df1ceb
11
CHANGELOG.md
11
CHANGELOG.md
@ -28,6 +28,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A prefixed reference (`type`/`base`/`itemType`/`ref`/`substitutionGroup`/`memberTypes`)
|
||||
whose namespace is never imported is now rejected on the cross-document skip path
|
||||
(XSD `src-resolve`). When a schema declares imports but loads no external document,
|
||||
the pool-based check stands down; a reference into a namespace that is neither the
|
||||
target, the XSD or XML namespace, nor named by any `xs:import` can resolve to no
|
||||
component and is flagged. To stay sound when the import closure is unknown, the
|
||||
check stands down whenever any external carries a `schemaLocation` (an unloaded
|
||||
located document may transitively import the referenced namespace), applying only
|
||||
when every external is location-less. XSTS invalid-schemas-accepted 43 -> 39
|
||||
(schZ011_a-d), valid-schemas-rejected held at 0, no other bucket moved.
|
||||
|
||||
- A non-nillable element may now carry no `xsi:nil` attribute at all, whatever its
|
||||
value (cvc-elt.3.1). Previously only `xsi:nil="true"` on a non-nillable element
|
||||
was rejected; `xsi:nil="false"` slipped through. `ComplexValidator.nilErrors` now
|
||||
|
||||
@ -60,6 +60,7 @@ extension PureXML.Schema.XSDParser {
|
||||
let simpleContentErrors = simpleContentBaseErrors(schema, in: context)
|
||||
if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) {
|
||||
return xsdErrors + simpleContentErrors
|
||||
+ undeclaredNamespaceReferenceErrors(schema, containers: containers)
|
||||
}
|
||||
let types = referenceBuiltins.union(context.simpleTypes.keys).union(context.complexTypeNodes.keys)
|
||||
let pools: [String: Set<String>] = [
|
||||
|
||||
96
Sources/Schema/SchemaUndeclaredNamespace.swift
Normal file
96
Sources/Schema/SchemaUndeclaredNamespace.swift
Normal file
@ -0,0 +1,96 @@
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// XSD 1.0 `src-resolve`, reduced to the part that holds even when no external
|
||||
/// document was loaded: a prefixed reference whose namespace is neither this
|
||||
/// schema's target namespace, the XSD or XML namespace, nor declared by any
|
||||
/// `xs:import` in the assembled set can resolve to no component, so it is
|
||||
/// unresolvable regardless. This runs only on the cross-document skip path.
|
||||
///
|
||||
/// Soundness requires that the import closure be fully *known*. It is known only
|
||||
/// when every `import`/`include`/`redefine` is location-less: there is nothing to
|
||||
/// load, so no unseen document can transitively import a further namespace. The
|
||||
/// moment any external carries a `schemaLocation` that did not load, the closure is
|
||||
/// unknown (that document might import the very namespace a reference names, as in
|
||||
/// corpus schZ004, where the main doc imports only `a` but `a`'s document imports
|
||||
/// `b`, making `b:b` resolvable), so the whole check stands down to stay lenient.
|
||||
static func undeclaredNamespaceReferenceErrors(_ schema: XSDTree, containers: [XSDTree]) -> [String] {
|
||||
let sources = containers.isEmpty ? [schema] : containers
|
||||
if hasLocatedExternal(sources) { return [] }
|
||||
var declared: Set<String> = [xsdNamespace]
|
||||
declared.formUnion(unloadedReferenceNamespaces)
|
||||
for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" {
|
||||
if let target = PureXML.Schema.XSDNode.attribute(container, "targetNamespace"), !target.isEmpty {
|
||||
declared.insert(target)
|
||||
}
|
||||
for node in descendants(container, named: "import") where node.name?.namespaceURI == xsdNamespace {
|
||||
if let namespace = PureXML.Schema.XSDNode.attribute(node, "namespace"), !namespace.isEmpty {
|
||||
declared.insert(namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
var errors: [String] = []
|
||||
for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" {
|
||||
collectUndeclaredNamespaceReferences(
|
||||
container,
|
||||
inheritedBindings: PureXML.Schema.XSDNode.namespaceBindings(of: container),
|
||||
declared: declared,
|
||||
into: &errors,
|
||||
)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func collectUndeclaredNamespaceReferences(
|
||||
_ node: XSDTree,
|
||||
inheritedBindings: [String: String],
|
||||
declared: Set<String>,
|
||||
into errors: inout [String],
|
||||
) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
let bindings = mergedNamespaceBindings(on: node, inherited: inheritedBindings)
|
||||
if node.name?.namespaceURI == xsdNamespace {
|
||||
for qname in foreignReferenceQNames(of: node) {
|
||||
guard PureXML.Schema.XSDNode.prefix(qname.trimmingXMLWhitespace()) != nil,
|
||||
let uri = referenceURI(for: qname, bindings: bindings), !uri.isEmpty,
|
||||
!declared.contains(uri)
|
||||
else { continue }
|
||||
errors.append("reference to '\(qname)' names namespace '\(uri)', which is not the target namespace and is not imported")
|
||||
}
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
collectUndeclaredNamespaceReferences(child, inheritedBindings: bindings, declared: declared, into: &errors)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether any `import`/`include`/`redefine` carries a `schemaLocation`. On the
|
||||
/// skip path nothing was loaded, so a located external is one that failed to load,
|
||||
/// leaving the import closure (and thus resolvability) unknown.
|
||||
private static func hasLocatedExternal(_ sources: [XSDTree]) -> Bool {
|
||||
for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" {
|
||||
for kind in ["import", "include", "redefine"] {
|
||||
for node in descendants(container, named: kind) where node.name?.namespaceURI == xsdNamespace {
|
||||
if let location = PureXML.Schema.XSDNode.attribute(node, "schemaLocation"), !location.isEmpty {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Every QName a node names through a resolving attribute: the single-QName
|
||||
/// `type`/`base`/`itemType`/`ref`/`substitutionGroup`, plus each token of a
|
||||
/// whitespace-separated `memberTypes`.
|
||||
private static func foreignReferenceQNames(of node: XSDTree) -> [String] {
|
||||
var references: [String] = []
|
||||
for attribute in ["type", "base", "itemType", "ref", "substitutionGroup"] {
|
||||
if let qname = PureXML.Schema.XSDNode.attribute(node, attribute) {
|
||||
references.append(qname)
|
||||
}
|
||||
}
|
||||
if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
|
||||
references.append(contentsOf: members.split(whereSeparator: \.isWhitespace).map(String.init))
|
||||
}
|
||||
return references
|
||||
}
|
||||
}
|
||||
93
Tests/SchemaForeignNamespaceReferenceTests.swift
Normal file
93
Tests/SchemaForeignNamespaceReferenceTests.swift
Normal file
@ -0,0 +1,93 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// src-resolve on the cross-document skip path (#185): when a schema declares
|
||||
/// imports but no external document is loaded (no `schemaLocation`, or it does not
|
||||
/// resolve), the full pool-based resolution stands down. A reference into an
|
||||
/// *imported* namespace must still stand down (its components may live in the
|
||||
/// unloaded document), but a reference into a namespace that is never imported can
|
||||
/// resolve nowhere under any loading and is unresolvable. Mirrors corpus schZ011.
|
||||
@Suite("XSD foreign-namespace reference resolvability (#185)")
|
||||
struct SchemaForeignNamespaceReferenceTests {
|
||||
private func compiles(_ schema: String) -> Bool {
|
||||
(try? PureXML.Schema.Document(schema)) != nil
|
||||
}
|
||||
|
||||
@Test("schZ011 shape: a reference into a never-imported namespace is rejected")
|
||||
func test_unimportedNamespaceReferenceRejected() {
|
||||
// imports a/b/c (no schemaLocation), references d:d where d is never imported.
|
||||
let schema = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="a" xmlns:b="b" xmlns:c="c" xmlns:d="d">
|
||||
<xs:import namespace="a"/>
|
||||
<xs:import namespace="b"/>
|
||||
<xs:import namespace="c"/>
|
||||
<xs:complexType name="uType">
|
||||
<xs:sequence>
|
||||
<xs:element ref="a:a"/>
|
||||
<xs:element ref="b:b"/>
|
||||
<xs:element ref="c:c"/>
|
||||
<xs:element ref="d:d"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(!compiles(schema), "d:d names an un-imported namespace and must be rejected")
|
||||
}
|
||||
|
||||
@Test("lenient guard: references into imported-but-unloaded namespaces stand down")
|
||||
func test_importedUnloadedNamespaceReferenceAccepted() {
|
||||
// Same shape without the offending d:d: every foreign reference is into an
|
||||
// imported namespace whose document was not loaded, so it must not be flagged.
|
||||
let schema = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="a" xmlns:b="b" xmlns:c="c">
|
||||
<xs:import namespace="a"/>
|
||||
<xs:import namespace="b"/>
|
||||
<xs:import namespace="c"/>
|
||||
<xs:complexType name="uType">
|
||||
<xs:sequence>
|
||||
<xs:element ref="a:a"/>
|
||||
<xs:element ref="b:b"/>
|
||||
<xs:element ref="c:c"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(compiles(schema), "imported-but-unloaded references must remain lenient")
|
||||
}
|
||||
|
||||
@Test("schZ004 shape on the skip path: a located (unloaded) import keeps the check lenient")
|
||||
func test_locatedUnloadedImportStandsDown() {
|
||||
// Mirrors corpus schZ004 (W3C: valid): the main doc imports only `a` (with a
|
||||
// schemaLocation), and references `b:b`. Namespace `b` is reachable transitively
|
||||
// because `a`'s document imports it. When that document does not load, the import
|
||||
// closure is unknown, so `b:b` must NOT be flagged. Compiled with no loader, so
|
||||
// the located import fails to resolve and the skip path is taken.
|
||||
let schema = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
targetNamespace="main" xmlns:m="main" xmlns:a="a" xmlns:b="b">
|
||||
<xs:import namespace="a" schemaLocation="schZ004a.xsd"/>
|
||||
<xs:complexType name="ct">
|
||||
<xs:sequence>
|
||||
<xs:element name="a" type="a:a"/>
|
||||
<xs:element name="b" type="b:b"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(compiles(schema), "b:b is transitively resolvable; an unloaded located import must stay lenient")
|
||||
}
|
||||
|
||||
@Test("a foreign type reference into a never-imported namespace is also rejected")
|
||||
func test_unimportedTypeReferenceRejected() {
|
||||
let schema = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="a" xmlns:z="z">
|
||||
<xs:import namespace="a"/>
|
||||
<xs:element name="e" type="z:t"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(!compiles(schema), "z:t names an un-imported namespace and must be rejected")
|
||||
}
|
||||
}
|
||||
@ -146,9 +146,9 @@ struct XSTSSuiteTests {
|
||||
/// model-group occurrence, identity-constraint content, derivation base kind,
|
||||
/// attribute/simple-type rules, composition, simpleContent inline-type checks,
|
||||
/// complexContent mixedness, and substitution-group anySimpleType checks.
|
||||
/// Substitution closure barriers and element-ref metadata moved 45 -> 43.
|
||||
/// Substitution barriers and element-ref metadata moved 45 -> 43; src-resolve unresolvable foreign-ref (#185) 43 -> 39.
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 43
|
||||
private let knownSchemaInvalidAccepted = 39
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 26), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// declared type (complex/atomic/list/union via the element-ref chain); anyType cannot stand in
|
||||
|
||||
Loading…
Reference in New Issue
Block a user