fix(schema): a ref resolving to the XSD namespace is undeclared

isUndeclaredReferenceRef fell through to a lenient accept when a ref or
substitutionGroup resolved to the XSD namespace but the schema did not target
it. The XSD namespace declares no referenceable user component (element,
group, attribute, attribute group), so such a reference is undeclared, the
asymmetry isUndeclaredReferenceType already handled for type/base.

This is reached when an unprefixed reference picks up a default xmlns of the
XSD namespace, so it is not in the target namespace where the schema's own
components live: xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="foo"
with <element ref="root"/> resolves to {XSD}root, not {foo}root (XSTS
xsd013/xsd014). A schema that itself targets the XSD namespace is the
inTargetNamespace case, and a genuinely loaded XSD-namespace document is the
foreignPools case before this branch, so both are unaffected.

Verified against the full W3C XSTS 2006-11-06 suite: invalid-schemas-accepted
29 -> 27, valid-schemas-rejected held at 0, no other bucket moved. Adds
reference tests for both reject cases and a qualified-ref guard.
This commit is contained in:
Mihaela Mihaljevic 2026-06-21 22:56:14 +02:00
parent 0a76658a28
commit 7efc64479b
4 changed files with 40 additions and 2 deletions

View File

@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A schema reference (`ref`/`substitutionGroup`) that resolves to the XSD namespace is now rejected as undeclared. The XSD namespace declares no referenceable user component, so `isUndeclaredReferenceType` already rejected a `type`/`base` in it, but `isUndeclaredReferenceRef` fell through to a lenient accept. This is reached when an unprefixed reference picks up a default `xmlns` of the XSD namespace, so it is not in the target namespace where the schema's own components live: e.g. `xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="foo"` with `<element ref="root"/>` resolves to `{XSD}root`, not `{foo}root`, and is undeclared. A schema that itself targets the XSD namespace, and a genuinely loaded XSD-namespace document, are unaffected. XSTS invalid-schemas-accepted 29 → 27 (xsd013/xsd014), valid-schemas-rejected held at 0.
- Unique Particle Attribution now checks a `complexContent` extension against its EFFECTIVE content model (the base type's content followed by the extension's), not just the extension's own particles. A base whose trailing particle can recur or end optionally competes with the extension's leading particles, so an element could match either across the extension boundary; that cross-boundary non-determinism was missed and the schema wrongly accepted. The base content is resolved and prepended only for a local (target-namespace), non-cyclic base chain; a foreign, built-in (`xs:anyType`), unresolved, or cyclic base leaves the check on the extension's own content, so no valid schema is newly rejected. XSTS invalid-schemas-accepted 31 → 29 (particlesZ022 and one more), valid-schemas-rejected held at 0.
- Unique Particle Attribution now detects overlapping `xs:any` wildcards involving `##other`. `wildcardsOverlap` returned false for any pair involving `##other`, so two competing wildcards whose namespaces actually intersect, two `##other` constraints (which always share infinitely many namespaces) or `##other` against an enumerated set naming a non-target, non-absent namespace, were not flagged as non-deterministic, and the schema was wrongly accepted. The overlap is now computed correctly: `##other`∩`##other` always overlaps, and `##other`∩enumerated overlaps iff the set names a namespace that is neither absent (`##local`) nor the target. Disjoint cases (`##other` against `##local` or against the target namespace) remain deterministic, so no valid schema is newly rejected. XSTS invalid-schemas-accepted 35 → 31 (wildI009/wildI013/wildI014 and one more), valid-schemas-rejected held at 0.
- The content-model matcher no longer blows up on a nested model group with a huge but

View File

@ -73,6 +73,16 @@ extension PureXML.Schema.XSDParser {
if let foreign = context.foreignPools[uri]?[poolName] {
return !foreign.contains(localPart)
}
if uri == xsdNamespace {
// The XSD namespace declares no referenceable user component (element,
// group, attribute, attribute group), so a reference resolving to it is
// undeclared. This is reached when an unprefixed reference picks up a
// default `xmlns` of the XSD namespace, so it is NOT in the target
// namespace (XSTS xsd013/xsd014); the schema that itself targets the XSD
// namespace is the `inTargetNamespace` case above, and a genuinely loaded
// XSD-namespace document is the `foreignPools` case just before.
return true
}
if uri == nil || uri == "" {
if context.chameleonNamespace, context.pools[poolName]?.contains(localPart) == true { return false }
if !(context.targetNamespace == nil || context.targetNamespace == "") { return true }

View File

@ -44,6 +44,33 @@ struct SchemaReferenceTests {
try compile("<xs:simpleType name=\"t\"><xs:restriction base=\" xs:string \"/></xs:simpleType>")
}
@Test("a ref resolving to the XSD namespace is undeclared")
func test_refResolvingToXSDNamespaceIsUndeclared() throws {
// The default xmlns is the XSD namespace, so an unprefixed `ref="root"`
// resolves to {XSD}root, not {foo}root, and the XSD namespace declares no
// referenceable user element: undeclared (XSTS xsd013/xsd014).
let refRoot = """
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:foo="foo" targetNamespace="foo">
<element name="root"><complexType><sequence><element ref="root"/></sequence></complexType></element>
</schema>
"""
#expect(throws: (any Error).self) { _ = try PureXML.Schema.Document(refRoot) }
let refUndef = """
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:foo="foo" targetNamespace="foo">
<element name="root"><complexType><sequence><element ref="undef"/></sequence></complexType></element>
</schema>
"""
#expect(throws: (any Error).self) { _ = try PureXML.Schema.Document(refUndef) }
// Guard: the same shape with the ref qualified to the target resolves, so the
// XSD-namespace rule does not reject a correctly-namespaced reference.
let qualified = """
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:foo="foo" targetNamespace="foo">
<element name="root"><complexType><sequence><element ref="foo:root"/></sequence></complexType></element>
</schema>
"""
_ = try PureXML.Schema.Document(qualified)
}
@Test("references are not checked when an import may supply them")
func test_importSkipsCheck() throws {
// A dangling reference into an imported namespace is tolerated: the default

View File

@ -146,9 +146,9 @@ struct XSTSSuiteTests {
/// model-group occurrence, identity-constraint content, derivation base kind, attribute/simple-type,
/// composition, simpleContent inline-type, complexContent mixedness, and substitution-group anySimpleType checks.
/// src-resolve (#185) 43->39; xsi:Not-Allowed (#183) 39->37; NameAndTypeOK restriction-only + cos-all-limited group-ref (#158) 37->35.
/// UPA: `##other` overlap 35->31 (wildI009/I013/I014+1); effective extension content model 31->29 (particlesZ022+1); valid-rejected held at 0.
/// UPA `##other` overlap 35->31; UPA effective extension model 31->29; ref to XSD namespace is undeclared 29->27 (xsd013/xsd014); valid-rejected held at 0.
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 29
private let knownSchemaInvalidAccepted = 27
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