fix(schema): resolve a simpleContent base through complex-type chains; invalid-instances-accepted 39 to 37 (#147)
A complex type's simpleContent derivation can name another complex type with simpleContent as its base. simpleContentType resolved a non-XSD, non-simpleType base via context.simpleTypes only, so a complex-type base (absent from that map) fell back to xs:string and left the content unconstrained: text invalid for the real base type was accepted (baseTD00101, where Test restricts Test2 which extends xs:int, yet "b" was accepted). simpleContentBaseType now resolves the base to an XSD built-in, a named simple type, or, when the base is a complex type with simpleContent, that type's own resolved simple type by recursing through complexTypeNodes (cycle-guarded by a visited set); the deriving node's facets are then applied over the resolved base's facets. So Test resolves to int and non-int text is rejected, while a valid int and the accumulated facets along the chain are honoured. invalid-instances-accepted 39 to 37, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. An adversarial review exercised facet accumulation, named-simpleType bases in the chain, multi-level and extension chains, and cycles, and found no over-rejection.
This commit is contained in:
parent
488f58701a
commit
ad80877d27
@ -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 `simpleContent` derivation whose base is another complex type with simpleContent now resolves through the chain to the underlying simple type, so the element's text is validated against it. A type restricting or extending a complex type that ultimately derives from a built-in (e.g. `Test` restricts `Test2` which extends `xs:int`) validates its content as that built-in rather than as unconstrained string; `baseTD00101`'s non-int text is now rejected. The base resolution recurses through `complexTypeNodes`, accumulating facets, with a cycle guard. invalid-instances-accepted 39 → 37, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#147): `use="prohibited"` on an attribute now excludes it from the complex type's effective `{attribute uses}` (XSD 1.0 §3.4.2) during instance validation, so a prohibited attribute is undeclared: its presence is rejected when no attribute wildcard admits it (attJ), and still admitted (and validated per the wildcard's `processContents`) when one does (attZ002). The prohibited declaration is kept in the type rather than dropped, so schema-validity checks still see it (the invalid-schemas-accepted count is unchanged). invalid-instances-accepted 43 → 39, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#147): an element may now carry at most one attribute whose type is `xs:ID` (XSD 1.0 §3.4.6). Two ID-typed attributes admitted through an `xs:anyAttribute` wildcard (the schema itself stays valid, as a type with two declared ID attribute uses is already a compile error) are rejected at instance time. The count covers both a matched declared use's type and the global declaration a matching wildcard admits, including a restriction of `xs:ID`; `xs:IDREF`/`xs:ENTITY` and a lone ID attribute are unaffected. invalid-instances-accepted 45 → 43, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
@ -367,24 +367,34 @@ extension PureXML.Schema.XSDParser {
|
||||
return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: mixed ? .mixed(particle) : .elementOnly(particle))
|
||||
}
|
||||
|
||||
private static func simpleContentType(_ node: XSDTree, _ context: XSDContext) -> SimpleType {
|
||||
private static func simpleContentType(_ node: XSDTree, _ context: XSDContext, visited: Set<String> = []) -> SimpleType {
|
||||
guard let inner = derivation(node) else { return SimpleType(base: .string) }
|
||||
let rawBase = XSDNode.attribute(inner, "base") ?? "string"
|
||||
let baseName = XSDNode.stripPrefix(rawBase)
|
||||
let bindings = PureXML.Schema.XSDParser.namespaceBindingsInScope(of: inner, defaultBindings: context.namespaceBindings)
|
||||
let uri = XSDNode.referenceNamespace(rawBase, bindings)
|
||||
|
||||
let base: BuiltinType
|
||||
let inheritedFacets: Facets
|
||||
if uri == PureXML.Schema.XSDParser.xsdNamespace {
|
||||
base = BuiltinType(rawValue: baseName) ?? .string
|
||||
inheritedFacets = Facets()
|
||||
} else {
|
||||
base = context.simpleTypes[baseName]?.base ?? .string
|
||||
inheritedFacets = context.simpleTypes[baseName]?.facets ?? Facets()
|
||||
}
|
||||
var facets = inheritedFacets
|
||||
let baseType = simpleContentBaseType(baseName, uri: uri, context, visited: visited)
|
||||
var facets = baseType.facets
|
||||
XSDSimpleParser.applyFacets(inner, into: &facets)
|
||||
return SimpleType(base: base, facets: facets)
|
||||
return SimpleType(base: baseType.base, facets: facets)
|
||||
}
|
||||
|
||||
/// The simple type a `simpleContent` derivation derives from: an XSD built-in, a
|
||||
/// named simple type, or, when the base is another complex type with simpleContent,
|
||||
/// that type's own resolved simple type (so `Test` restricting `Test2` extending
|
||||
/// `xs:int` resolves to `int`). A cycle in the base chain falls back to `string`.
|
||||
private static func simpleContentBaseType(_ baseName: String, uri: String?, _ context: XSDContext, visited: Set<String>) -> SimpleType {
|
||||
if uri == PureXML.Schema.XSDParser.xsdNamespace {
|
||||
return SimpleType(base: BuiltinType(rawValue: baseName) ?? .string)
|
||||
}
|
||||
if let simple = context.simpleTypes[baseName] {
|
||||
return simple
|
||||
}
|
||||
guard !visited.contains(baseName),
|
||||
let complexNode = context.complexTypeNodes[baseName],
|
||||
let baseSimpleContent = XSDNode.firstChild(complexNode, named: "simpleContent")
|
||||
else { return SimpleType(base: .string) }
|
||||
return simpleContentType(baseSimpleContent, context, visited: visited.union([baseName]))
|
||||
}
|
||||
}
|
||||
|
||||
38
Tests/SchemaSimpleContentChainTests.swift
Normal file
38
Tests/SchemaSimpleContentChainTests.swift
Normal file
@ -0,0 +1,38 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// A `simpleContent` derivation whose base is another complex type with
|
||||
/// simpleContent resolves through the chain to the underlying simple type, so the
|
||||
/// element's text is validated against it. A type restricting (or extending) a
|
||||
/// complex type that ultimately derives from `xs:int` validates its content as an
|
||||
/// int, not as unconstrained string (W3C sun baseTD00101).
|
||||
@Suite("simpleContent base chain")
|
||||
struct SchemaSimpleContentChainTests {
|
||||
private func schema(method: String) throws -> PureXML.Schema.Document {
|
||||
try PureXML.Schema.Document("""
|
||||
<xsd:schema xmlns="u" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="u">
|
||||
<xsd:element name="root" type="Test"/>
|
||||
<xsd:complexType name="Test"><xsd:simpleContent>
|
||||
<xsd:\(method) base="Test2"/>
|
||||
</xsd:simpleContent></xsd:complexType>
|
||||
<xsd:complexType name="Test2"><xsd:simpleContent>
|
||||
<xsd:extension base="xsd:int"/>
|
||||
</xsd:simpleContent></xsd:complexType>
|
||||
</xsd:schema>
|
||||
""")
|
||||
}
|
||||
|
||||
@Test("text invalid for the chained int base is rejected, valid int accepted")
|
||||
func test_restrictionChainValidatesAsInt() throws {
|
||||
let restriction = try schema(method: "restriction")
|
||||
#expect(try !restriction.validate(#"<root xmlns="u">b</root>"#).isEmpty)
|
||||
#expect(try restriction.validate(#"<root xmlns="u">42</root>"#).isEmpty)
|
||||
}
|
||||
|
||||
@Test("an extension chain to int also validates the content as int")
|
||||
func test_extensionChainValidatesAsInt() throws {
|
||||
let extension0 = try schema(method: "extension")
|
||||
#expect(try !extension0.validate(#"<root xmlns="u">b</root>"#).isEmpty)
|
||||
#expect(try extension0.validate(#"<root xmlns="u">42</root>"#).isEmpty)
|
||||
}
|
||||
}
|
||||
@ -150,13 +150,13 @@ struct XSTSSuiteTests {
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 69
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 39), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// Instance bucket (133 -> 37), 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 = 39
|
||||
private let knownInstanceInvalidAccepted = 37
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user