fix(schema): a type attribute and an inline anonymous type are mutually exclusive
An <xs:element> or <xs:attribute> may not carry both a type attribute and an inline anonymous type definition (src-element.3 / src-attribute.3.1): an attribute may have only an inline simpleType, an element a simpleType or complexType. Such schemas were accepted. Only an inline type in the XSD namespace counts; a foreign-namespace child whose local name happens to be complexType/simpleType is not the component's own type definition (foreign content is not structurally checked), so it is not flagged (an adversarial critic's finding, fixed with the namespace guard and a test). The attribute-applicability function and its allowed-attributes table were extracted to SchemaAttributeApplicability.swift (a pure move) to keep SchemaStructure.swift within the file-length limit. XSTS: invalid schemas accepted 495 to 489 (addB081, attKa011, attKb014, attKc014, typedef00501m2, typedef00502m2 now rejected), no other bucket raised.
This commit is contained in:
parent
067f04184f
commit
0142ecb35e
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A `type` attribute and an inline anonymous type definition are now mutually exclusive on an element or attribute (XSD 1.0 `src-element.3` / `src-attribute.3.1`, #170). An `<xs:element>` or `<xs:attribute>` carrying both a `type=` and an inline `<simpleType>` (or `<complexType>`, for an element) is rejected; previously it was accepted. Only an inline type in the XSD namespace counts, so a foreign-namespace child that happens to be named `complexType`/`simpleType` is not mistaken for the component's own type. XSTS: invalid schemas accepted 495 to 489, no other bucket raised.
|
||||
- Attribute-use uniqueness and the single-ID rule are now enforced (XSD 1.0 `ct-props-correct.4` / `ag-props-correct.2` / `ct-props-correct.5`, #162). A complex type's or attribute group's attribute-use set (flattened through `attributeGroup` references) may not contain two distinct declarations with the same name and namespace, nor more than one attribute whose type is or derives from `xs:ID`. Declarations are deduplicated by the identity of their declaring node, so the same declaration reached by two paths (a diamond of attribute-group references) is one use, not a clash; names carry their resolved namespace, so a local attribute and a reference to a same-local-name attribute in an imported namespace do not collide. XSTS: invalid schemas accepted 505 to 495, no other bucket raised. Disclosed under-rejections: the set inherited through extension is not composed in, and a duplicate that requires an unprefixed reference in a schema with no default namespace is not detected.
|
||||
- Circular references among model groups, attribute groups, and substitution-group affiliations are now rejected (XSD 1.0 `mg-props-correct.2`, `ag-props-correct.3`, `e-props-correct.6`, #168). A model group may not contain itself, an attribute group may not reference itself transitively, and a substitution chain may not loop. The model-group check follows references only within a definition's own compositor tree, stopping at element and type boundaries: a group referenced inside an element's content is a recursive data structure (permitted), not the group containing itself. A reference is followed only when it resolves to the schema's own target namespace, and a group or attribute group redefined in `xs:redefine` (whose single self-reference is the legal redefinition) ends the chain. XSTS: invalid schemas accepted 516 to 505, no other bucket raised. A genuine cycle introduced across two redefined definitions is a disclosed under-rejection.
|
||||
- Circular type derivation is now rejected at compile time (XSD 1.0 `ct-props-correct.3` / `st-props-correct.2`, #168). A complex or simple type may not derive, transitively through its base, from itself; previously such a schema was accepted. Only the base chain is followed, so a type whose element content recurses (an element of its own type, the normal recursive-data-structure case) is correctly permitted, and a type redefined in `xs:redefine` (whose base legitimately names its former self) and the ur-types (`anyType`/`anySimpleType`/`anyAtomicType`, the roots the schema-for-schemas bootstraps) end the chain rather than appear as cycles. The base is followed only when it resolves to the schema's own target namespace, so a type whose base is a same-local-name type in another namespace is not mistaken for a self-cycle. XSTS: invalid schemas accepted 519 to 516, no other bucket raised. Union/list (`itemType`/`memberTypes`) and genuine cross-namespace derivation cycles are a disclosed under-rejection (the engine resolves by local name).
|
||||
|
||||
91
Sources/Schema/SchemaAttributeApplicability.swift
Normal file
91
Sources/Schema/SchemaAttributeApplicability.swift
Normal file
@ -0,0 +1,91 @@
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// The unprefixed attributes each XSD component admits (XSD 1.0 schema for
|
||||
/// schemas). The element/attribute entries are the permissive union across
|
||||
/// global and local use (so a context-only attribute like `form` or
|
||||
/// `minOccurs` is never wrongly flagged); `id` is admitted everywhere. An
|
||||
/// attribute from a foreign namespace (prefixed) is always allowed and is not
|
||||
/// listed here.
|
||||
private static let allowedAttributes: [String: Set<String>] = {
|
||||
let facetAttrs: Set = ["value", "fixed", "id"]
|
||||
var table: [String: Set<String>] = [
|
||||
"schema": ["targetNamespace", "version", "finalDefault", "blockDefault", "attributeFormDefault", "elementFormDefault", "id"],
|
||||
"element": ["name", "ref", "type", "minOccurs", "maxOccurs", "default", "fixed", "nillable", "abstract", "substitutionGroup", "final", "block", "form", "id"],
|
||||
"attribute": ["name", "ref", "type", "use", "default", "fixed", "form", "id"],
|
||||
"complexType": ["name", "abstract", "final", "block", "mixed", "id"],
|
||||
"simpleType": ["name", "final", "id"],
|
||||
"complexContent": ["mixed", "id"],
|
||||
"simpleContent": ["id"],
|
||||
"restriction": ["base", "id"],
|
||||
"extension": ["base", "id"],
|
||||
"group": ["name", "ref", "minOccurs", "maxOccurs", "id"],
|
||||
"attributeGroup": ["name", "ref", "id"],
|
||||
"sequence": ["minOccurs", "maxOccurs", "id"],
|
||||
"choice": ["minOccurs", "maxOccurs", "id"],
|
||||
"all": ["minOccurs", "maxOccurs", "id"],
|
||||
"any": ["namespace", "processContents", "minOccurs", "maxOccurs", "id"],
|
||||
"anyAttribute": ["namespace", "processContents", "id"],
|
||||
"unique": ["name", "id"],
|
||||
"key": ["name", "id"],
|
||||
"keyref": ["name", "refer", "id"],
|
||||
"selector": ["xpath", "id"],
|
||||
"field": ["xpath", "id"],
|
||||
"list": ["itemType", "id"],
|
||||
"union": ["memberTypes", "id"],
|
||||
"import": ["namespace", "schemaLocation", "id"],
|
||||
"include": ["schemaLocation", "id"],
|
||||
"redefine": ["schemaLocation", "id"],
|
||||
"notation": ["name", "public", "system", "id"],
|
||||
"annotation": ["id"],
|
||||
]
|
||||
for facet in ["minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits", "fractionDigits", "length", "minLength", "maxLength", "whiteSpace"] {
|
||||
table[facet] = facetAttrs
|
||||
}
|
||||
for facet in ["enumeration", "pattern"] {
|
||||
table[facet] = ["value", "id"]
|
||||
}
|
||||
return table
|
||||
}()
|
||||
|
||||
/// Findings for the unprefixed attributes on `node`: any not admitted by its
|
||||
/// component, the `ref` exclusions (`ref` excludes `name` and `type`), and the
|
||||
/// `type`-excludes-inline-type exclusion. A prefixed (foreign-namespace)
|
||||
/// attribute and a namespace declaration are not the component's own and are
|
||||
/// not checked.
|
||||
static func attributeApplicabilityErrors(_ node: XSDTree, local: String) -> [String] {
|
||||
guard let allowed = allowedAttributes[local] else { return [] }
|
||||
var errors: [String] = []
|
||||
var present: Set<String> = []
|
||||
for attribute in node.attributes where attribute.name.prefix == nil && attribute.name.localName != "xmlns" {
|
||||
let name = attribute.name.localName
|
||||
present.insert(name)
|
||||
if !allowed.contains(name) {
|
||||
errors.append("attribute '\(name)' is not allowed on '\(local)'")
|
||||
}
|
||||
}
|
||||
if present.contains("ref"), present.contains("name") {
|
||||
errors.append("'\(local)' may not have both 'ref' and 'name'")
|
||||
}
|
||||
if present.contains("ref"), present.contains("type") {
|
||||
errors.append("'\(local)' may not have both 'ref' and 'type'")
|
||||
}
|
||||
// src-element.3 / src-attribute.3-4: a `type` attribute and an inline
|
||||
// anonymous type definition are mutually exclusive. An attribute may carry
|
||||
// only an inline `simpleType`; an element may carry a `simpleType` or
|
||||
// `complexType`.
|
||||
if present.contains("type"), local == "element" || local == "attribute", hasInlineType(node, local) {
|
||||
errors.append("'\(local)' may not have both a 'type' attribute and an inline type definition")
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func hasInlineType(_ node: XSDTree, _ local: String) -> Bool {
|
||||
PureXML.Schema.XSDNode.elementChildren(node).contains { child in
|
||||
// Only an inline type in the XSD namespace counts; a foreign-namespace
|
||||
// child that happens to be named `complexType`/`simpleType` is not the
|
||||
// component's own type definition (foreign content is not checked here).
|
||||
guard child.name?.namespaceURI == xsdNamespace else { return false }
|
||||
let kind = PureXML.Schema.XSDNode.localName(child)
|
||||
return kind == "simpleType" || (local == "element" && kind == "complexType")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,77 +61,6 @@ extension PureXML.Schema.XSDParser {
|
||||
return errors
|
||||
}
|
||||
|
||||
/// The unprefixed attributes each XSD component admits (XSD 1.0 schema for
|
||||
/// schemas). The element/attribute entries are the permissive union across
|
||||
/// global and local use (so a context-only attribute like `form` or
|
||||
/// `minOccurs` is never wrongly flagged); `id` is admitted everywhere. An
|
||||
/// attribute from a foreign namespace (prefixed) is always allowed and is not
|
||||
/// listed here.
|
||||
private static let allowedAttributes: [String: Set<String>] = {
|
||||
let facetAttrs: Set = ["value", "fixed", "id"]
|
||||
var table: [String: Set<String>] = [
|
||||
"schema": ["targetNamespace", "version", "finalDefault", "blockDefault", "attributeFormDefault", "elementFormDefault", "id"],
|
||||
"element": ["name", "ref", "type", "minOccurs", "maxOccurs", "default", "fixed", "nillable", "abstract", "substitutionGroup", "final", "block", "form", "id"],
|
||||
"attribute": ["name", "ref", "type", "use", "default", "fixed", "form", "id"],
|
||||
"complexType": ["name", "abstract", "final", "block", "mixed", "id"],
|
||||
"simpleType": ["name", "final", "id"],
|
||||
"complexContent": ["mixed", "id"],
|
||||
"simpleContent": ["id"],
|
||||
"restriction": ["base", "id"],
|
||||
"extension": ["base", "id"],
|
||||
"group": ["name", "ref", "minOccurs", "maxOccurs", "id"],
|
||||
"attributeGroup": ["name", "ref", "id"],
|
||||
"sequence": ["minOccurs", "maxOccurs", "id"],
|
||||
"choice": ["minOccurs", "maxOccurs", "id"],
|
||||
"all": ["minOccurs", "maxOccurs", "id"],
|
||||
"any": ["namespace", "processContents", "minOccurs", "maxOccurs", "id"],
|
||||
"anyAttribute": ["namespace", "processContents", "id"],
|
||||
"unique": ["name", "id"],
|
||||
"key": ["name", "id"],
|
||||
"keyref": ["name", "refer", "id"],
|
||||
"selector": ["xpath", "id"],
|
||||
"field": ["xpath", "id"],
|
||||
"list": ["itemType", "id"],
|
||||
"union": ["memberTypes", "id"],
|
||||
"import": ["namespace", "schemaLocation", "id"],
|
||||
"include": ["schemaLocation", "id"],
|
||||
"redefine": ["schemaLocation", "id"],
|
||||
"notation": ["name", "public", "system", "id"],
|
||||
"annotation": ["id"],
|
||||
]
|
||||
for facet in ["minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits", "fractionDigits", "length", "minLength", "maxLength", "whiteSpace"] {
|
||||
table[facet] = facetAttrs
|
||||
}
|
||||
for facet in ["enumeration", "pattern"] {
|
||||
table[facet] = ["value", "id"]
|
||||
}
|
||||
return table
|
||||
}()
|
||||
|
||||
/// Findings for the unprefixed attributes on `node`: any not admitted by its
|
||||
/// component, and the `ref` exclusions (`ref` excludes `name` and `type`). A
|
||||
/// prefixed (foreign-namespace) attribute and a namespace declaration are not
|
||||
/// the component's own and are not checked.
|
||||
private static func attributeApplicabilityErrors(_ node: XSDTree, local: String) -> [String] {
|
||||
guard let allowed = allowedAttributes[local] else { return [] }
|
||||
var errors: [String] = []
|
||||
var present: Set<String> = []
|
||||
for attribute in node.attributes where attribute.name.prefix == nil && attribute.name.localName != "xmlns" {
|
||||
let name = attribute.name.localName
|
||||
present.insert(name)
|
||||
if !allowed.contains(name) {
|
||||
errors.append("attribute '\(name)' is not allowed on '\(local)'")
|
||||
}
|
||||
}
|
||||
if present.contains("ref"), present.contains("name") {
|
||||
errors.append("'\(local)' may not have both 'ref' and 'name'")
|
||||
}
|
||||
if present.contains("ref"), present.contains("type") {
|
||||
errors.append("'\(local)' may not have both 'ref' and 'type'")
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func collectStructure(_ node: XSDTree, into errors: inout [String]) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
|
||||
41
Tests/SchemaTypeExclusivityTests.swift
Normal file
41
Tests/SchemaTypeExclusivityTests.swift
Normal file
@ -0,0 +1,41 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("XSD type attribute excludes an inline type")
|
||||
struct SchemaTypeExclusivityTests {
|
||||
private let xsd = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
|
||||
|
||||
private func compiles(_ body: String) -> Bool {
|
||||
(try? PureXML.Schema.Document("<xs:schema \(xsd)>\(body)</xs:schema>")) != nil
|
||||
}
|
||||
|
||||
@Test("type attribute and an inline type definition are mutually exclusive")
|
||||
func test_typeAndInlineTypeRejected() {
|
||||
// Element with both type= and an inline simpleType (src-element.3).
|
||||
#expect(!compiles("<xs:element name=\"e\" type=\"xs:string\"><xs:simpleType>"
|
||||
+ "<xs:restriction base=\"xs:string\"/></xs:simpleType></xs:element>"))
|
||||
// Element with both type= and an inline complexType.
|
||||
#expect(!compiles("<xs:element name=\"e\" type=\"xs:string\"><xs:complexType><xs:sequence/></xs:complexType></xs:element>"))
|
||||
// Attribute with both type= and an inline simpleType (src-attribute.3.1).
|
||||
#expect(!compiles("<xs:complexType name=\"T\"><xs:attribute name=\"a\" type=\"xs:string\">"
|
||||
+ "<xs:simpleType><xs:restriction base=\"xs:string\"/></xs:simpleType></xs:attribute></xs:complexType>"))
|
||||
}
|
||||
|
||||
@Test("type alone, or an inline type alone, compiles")
|
||||
func test_eitherAloneAccepted() {
|
||||
#expect(compiles("<xs:element name=\"e\" type=\"xs:string\"/>"))
|
||||
#expect(compiles("<xs:element name=\"e\"><xs:simpleType><xs:restriction base=\"xs:string\"/></xs:simpleType></xs:element>"))
|
||||
#expect(compiles("<xs:complexType name=\"T\"><xs:attribute name=\"a\" type=\"xs:string\"/></xs:complexType>"))
|
||||
// type= alongside an identity constraint (not a type) is legal.
|
||||
#expect(compiles("<xs:element name=\"e\" type=\"xs:string\"><xs:unique name=\"u\"><xs:selector xpath=\".\"/><xs:field xpath=\"@x\"/></xs:unique></xs:element>"))
|
||||
}
|
||||
|
||||
@Test("A foreign-namespace child named simpleType/complexType is not the component's inline type")
|
||||
func test_foreignNamespaceChildNotInlineType() {
|
||||
// A foreign child whose local name happens to be complexType is not an XSD
|
||||
// inline type, so type= alongside it is not the type/inline-type clash.
|
||||
let schema = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:foo=\"urn:foo\">"
|
||||
+ "<xs:element name=\"e\" type=\"xs:string\"><foo:complexType/></xs:element></xs:schema>"
|
||||
#expect((try? PureXML.Schema.Document(schema)) != nil)
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ struct XSTSSuiteTests {
|
||||
/// XSTS` (debug is far slower and the corpus is large). Per-case deviations
|
||||
/// are written to /tmp/xsts-failures.txt for the burn-down.
|
||||
private let knownSchemaValidRejected = 1
|
||||
private let knownSchemaInvalidAccepted = 495
|
||||
private let knownSchemaInvalidAccepted = 489
|
||||
private let knownInstanceValidRejected = 180
|
||||
private let knownInstanceInvalidAccepted = 160
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user