fix(schema): check component name and identity constraint uniqueness across all containers
This commit is contained in:
parent
d0e0b96de9
commit
4a7e23186d
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Enforce cross-container global component-name and identity-constraint uniqueness constraints (XSD 1.0 Structures §3.14). Within a schema (including all included and imported schemas), global type names (simpleType and complexType sharing one symbol space), global element names, global attribute names, named model groups, and named attribute groups must be unique within their respective symbol spaces and target namespaces. Identity constraints (unique, key, keyref) must have unique names across all target namespaces in the entire schema document. Circular and duplicate imports/includes are handled namespace-aware without causing false-positive validations on valid schemas. XSTS: invalid schemas accepted 316 to 309, no other bucket raised.
|
||||
- Enforce substitution group exclusions based on element final and schema finalDefault attributes (XSD 1.0 Structures §3.3.6 / Schema Component Constraint: Substitution Group OK (Transitive)), including support for element declarations with inline anonymous types. The entire derivation path from the member type to the head type is checked; if any step contains a derivation method prohibited by the head element's final or the schema's finalDefault, the substitution is rejected. XSTS: invalid schemas accepted 323 to 316, no other bucket raised.
|
||||
- Enforce targetNamespace scope matching on local schema references (XSD 1.0 Structures §3.14). If a schema has a targetNamespace declared, any local type, element, attribute, group, or attributeGroup reference without a namespace prefix (resolving to no namespace) is rejected as undeclared unless a default namespace matching the targetNamespace is declared on the element, since all global components defined in the document belong to the targetNamespace. XSTS: invalid schemas accepted 325 to 323, no other bucket raised.
|
||||
- Enforce variety constraints on list itemType and union memberTypes (XSD 1.0 Datatypes §3.3.1 / §3.4.1). A list's itemType must be atomic or union of atomic types, and cannot be a list or a union containing a list, and cannot resolve to 'anySimpleType'. Resolves namespace prefixes dynamically in scope to handle local prefix declarations without false positives. XSTS: invalid schemas accepted 327 to 325, no other bucket raised.
|
||||
|
||||
@ -45,7 +45,8 @@ extension PureXML.Schema.XSDParser {
|
||||
/// type derivation. The explicit return type keeps the type-checker from
|
||||
/// inferring a long concatenation.
|
||||
static func consistencyErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ containers: [XSDTree]) -> [String] {
|
||||
let structural = idAttributeErrors(schema) + structureErrors(schema) + componentNameErrors(schema)
|
||||
let structural = idAttributeErrors(schema) + structureErrors(schema)
|
||||
+ componentNameErrors(schema, containers, context.targetNamespace)
|
||||
+ simpleTypeFinalErrors(schema)
|
||||
let determinism = PureXML.Schema.ContentModelDeterminism.violations(in: schema, context: context)
|
||||
let cycles = derivationCycleErrors(containers, context.namespaceBindings, context.targetNamespace)
|
||||
|
||||
@ -9,20 +9,96 @@ extension PureXML.Schema.XSDParser {
|
||||
/// Only the document's own globals are examined (`xs:redefine` children and
|
||||
/// included documents are not direct globals here), so a redefinition is not
|
||||
/// mistaken for a clash.
|
||||
static func componentNameErrors(_ schema: XSDTree) -> [String] {
|
||||
let globals = PureXML.Schema.XSDNode.elementChildren(schema)
|
||||
.filter { $0.name?.namespaceURI == xsdNamespace }
|
||||
var errors: [String] = []
|
||||
errors += duplicateNames(of: ["simpleType", "complexType"], in: globals, label: "type")
|
||||
errors += duplicateNames(of: ["element"], in: globals, label: "element")
|
||||
errors += duplicateNames(of: ["attribute"], in: globals, label: "attribute")
|
||||
errors += duplicateNames(of: ["group"], in: globals, label: "model group")
|
||||
errors += duplicateNames(of: ["attributeGroup"], in: globals, label: "attribute group")
|
||||
errors += identityConstraintNameErrors(schema)
|
||||
static func componentNameErrors(_ schema: XSDTree, _ containers: [XSDTree], _ mainTargetNamespace: String?) -> [String] {
|
||||
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: mainTargetNamespace)
|
||||
let uniqueIndices = findUniqueIndices(in: containers, namespaceMap: namespaceMap, mainTargetNamespace: mainTargetNamespace)
|
||||
|
||||
var errors = checkDuplicateGlobals(
|
||||
in: containers,
|
||||
indices: uniqueIndices,
|
||||
namespaceMap: namespaceMap,
|
||||
mainTargetNamespace: mainTargetNamespace,
|
||||
)
|
||||
errors += identityConstraintNameErrors(
|
||||
containers,
|
||||
uniqueIndices,
|
||||
namespaceMap: namespaceMap,
|
||||
mainTargetNamespace: mainTargetNamespace,
|
||||
)
|
||||
errors += keyrefReferErrors(schema)
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func findUniqueIndices(
|
||||
in containers: [XSDTree],
|
||||
namespaceMap: [Int: String?],
|
||||
mainTargetNamespace: String?,
|
||||
) -> [Int] {
|
||||
var uniqueIndices: [Int] = []
|
||||
for index in containers.indices {
|
||||
let container = containers[index]
|
||||
let namespaceURI = namespaceMap[index] ?? mainTargetNamespace
|
||||
let alreadyExists = uniqueIndices.contains { prevIndex in
|
||||
let prevNS = namespaceMap[prevIndex] ?? mainTargetNamespace
|
||||
return prevNS == namespaceURI && isStructurallyEqual(containers[prevIndex], container)
|
||||
}
|
||||
if !alreadyExists {
|
||||
uniqueIndices.append(index)
|
||||
}
|
||||
}
|
||||
return uniqueIndices
|
||||
}
|
||||
|
||||
private struct ComponentKey: Hashable {
|
||||
let kind: String
|
||||
let targetNamespace: String?
|
||||
let name: String
|
||||
}
|
||||
|
||||
private static func checkDuplicateGlobals(
|
||||
in containers: [XSDTree],
|
||||
indices: [Int],
|
||||
namespaceMap: [Int: String?],
|
||||
mainTargetNamespace: String?,
|
||||
) -> [String] {
|
||||
var errors: [String] = []
|
||||
var seenComponents: Set<ComponentKey> = []
|
||||
for index in indices {
|
||||
let container = containers[index]
|
||||
guard PureXML.Schema.XSDNode.localName(container) != "redefine" else { continue }
|
||||
let namespaceURI = namespaceMap[index] ?? mainTargetNamespace
|
||||
let globals = PureXML.Schema.XSDNode.elementChildren(container).filter {
|
||||
$0.name?.namespaceURI == xsdNamespace
|
||||
}
|
||||
for global in globals {
|
||||
let localName = PureXML.Schema.XSDNode.localName(global) ?? ""
|
||||
guard let kind = componentKind(for: localName),
|
||||
let name = PureXML.Schema.XSDNode.attribute(global, "name")?.trimmingXMLWhitespace()
|
||||
else {
|
||||
continue
|
||||
}
|
||||
let key = ComponentKey(kind: kind, targetNamespace: namespaceURI, name: name)
|
||||
if !seenComponents.insert(key).inserted {
|
||||
let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace"
|
||||
let kindLabel = kind == "group" ? "model group" : (kind == "attributeGroup" ? "attribute group" : kind)
|
||||
errors.append("duplicate \(kindLabel) name '\(name)'\(nsStr)")
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func componentKind(for localName: String) -> String? {
|
||||
switch localName {
|
||||
case "simpleType", "complexType": "type"
|
||||
case "element": "element"
|
||||
case "attribute": "attribute"
|
||||
case "group": "group"
|
||||
case "attributeGroup": "attributeGroup"
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
private struct KeyrefInfo {
|
||||
let name: String
|
||||
let refer: String
|
||||
@ -81,34 +157,38 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Duplicate `name` values among the `kinds` components in `nodes`.
|
||||
private static func duplicateNames(of kinds: Set<String>, in nodes: [XSDTree], label: String) -> [String] {
|
||||
var seen: Set<String> = []
|
||||
var errors: [String] = []
|
||||
for node in nodes where kinds.contains(PureXML.Schema.XSDNode.localName(node) ?? "") {
|
||||
guard let name = PureXML.Schema.XSDNode.attribute(node, "name")?.trimmingXMLWhitespace() else { continue }
|
||||
if !seen.insert(name).inserted {
|
||||
errors.append("duplicate \(label) name '\(name)'")
|
||||
}
|
||||
private static func identityConstraintNameErrors(
|
||||
_ containers: [XSDTree],
|
||||
_ uniqueIndices: [Int],
|
||||
namespaceMap: [Int: String?],
|
||||
mainTargetNamespace: String?,
|
||||
) -> [String] {
|
||||
struct KeyrefNameKey: Hashable {
|
||||
let targetNamespace: String?
|
||||
let name: String
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// Duplicate identity-constraint names anywhere in the schema document.
|
||||
private static func identityConstraintNameErrors(_ schema: XSDTree) -> [String] {
|
||||
var seen: Set<String> = []
|
||||
var seen: Set<KeyrefNameKey> = []
|
||||
var errors: [String] = []
|
||||
func walk(_ node: XSDTree) {
|
||||
func walk(_ node: XSDTree, namespaceURI: String?) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
if let name = identityConstraintName(node), !seen.insert(name).inserted {
|
||||
errors.append("duplicate identity constraint name '\(name)'")
|
||||
if let name = identityConstraintName(node) {
|
||||
let key = KeyrefNameKey(targetNamespace: namespaceURI, name: name)
|
||||
if !seen.insert(key).inserted {
|
||||
let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace"
|
||||
errors.append("duplicate identity constraint name '\(name)'\(nsStr)")
|
||||
}
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
walk(child)
|
||||
walk(child, namespaceURI: namespaceURI)
|
||||
}
|
||||
}
|
||||
walk(schema)
|
||||
for index in uniqueIndices {
|
||||
let container = containers[index]
|
||||
guard PureXML.Schema.XSDNode.localName(container) != "redefine" else { continue }
|
||||
let namespaceURI = namespaceMap[index] ?? mainTargetNamespace
|
||||
walk(container, namespaceURI: namespaceURI)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
@ -121,4 +201,85 @@ extension PureXML.Schema.XSDParser {
|
||||
else { return nil }
|
||||
return PureXML.Schema.XSDNode.attribute(node, "name")?.trimmingXMLWhitespace()
|
||||
}
|
||||
|
||||
private static func isStructurallyEqual(_ lhs: XSDTree, _ rhs: XSDTree) -> Bool {
|
||||
if lhs.name != rhs.name { return false }
|
||||
if lhs.attributes.count != rhs.attributes.count { return false }
|
||||
for attr in lhs.attributes {
|
||||
guard let rhsVal = rhs.attributes.first(where: { $0.name == attr.name })?.value,
|
||||
rhsVal == attr.value
|
||||
else { return false }
|
||||
}
|
||||
let lhsChildren = PureXML.Schema.XSDNode.elementChildren(lhs)
|
||||
let rhsChildren = PureXML.Schema.XSDNode.elementChildren(rhs)
|
||||
if lhsChildren.count != rhsChildren.count { return false }
|
||||
guard !zip(lhsChildren, rhsChildren).contains(where: { !isStructurallyEqual($0, $1) }) else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
private static func resolveContainerNamespaces(_ containers: [XSDTree], mainTargetNamespace: String?) -> [Int: String?] {
|
||||
var resolved: [Int: String?] = [:]
|
||||
guard !containers.isEmpty else { return resolved }
|
||||
let mainIndex = containers.count - 1
|
||||
resolved[mainIndex] = mainTargetNamespace
|
||||
for parentIndex in containers.indices.reversed() {
|
||||
guard let parentNamespace = resolved[parentIndex] else { continue }
|
||||
let parent = containers[parentIndex]
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(parent) {
|
||||
let kind = PureXML.Schema.XSDNode.localName(child)
|
||||
guard kind == "include" || kind == "import" || kind == "redefine",
|
||||
let childKind = kind
|
||||
else {
|
||||
continue
|
||||
}
|
||||
let targetNamespaceURI: String? = if childKind == "import" {
|
||||
PureXML.Schema.XSDNode.attribute(child, "namespace")
|
||||
} else {
|
||||
parentNamespace
|
||||
}
|
||||
let expectedAttrNamespace: String? = childKind == "import" ? PureXML.Schema.XSDNode.attribute(child, "namespace") : parentNamespace
|
||||
let foundIndex = findMatchingContainerIndex(
|
||||
in: containers,
|
||||
before: parentIndex,
|
||||
kind: childKind,
|
||||
namespaces: (parent: parentNamespace, expected: expectedAttrNamespace),
|
||||
resolved: resolved,
|
||||
)
|
||||
if let foundIndex {
|
||||
resolved[foundIndex] = targetNamespaceURI
|
||||
}
|
||||
}
|
||||
}
|
||||
for index in containers.indices where resolved[index] == nil {
|
||||
let container = containers[index]
|
||||
if let attrNS = PureXML.Schema.XSDNode.attribute(container, "targetNamespace") {
|
||||
resolved[index] = attrNS
|
||||
} else {
|
||||
resolved[index] = mainTargetNamespace
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
private static func findMatchingContainerIndex(
|
||||
in containers: [XSDTree],
|
||||
before parentIndex: Int,
|
||||
kind: String,
|
||||
namespaces: (parent: String?, expected: String?),
|
||||
resolved: [Int: String?],
|
||||
) -> Int? {
|
||||
for index in (0 ..< parentIndex).reversed() where resolved[index] == nil {
|
||||
let container = containers[index]
|
||||
let attrNS = PureXML.Schema.XSDNode.attribute(container, "targetNamespace")
|
||||
let isMatch: Bool = if kind == "import" {
|
||||
attrNS == namespaces.expected
|
||||
} else {
|
||||
attrNS == nil || attrNS == namespaces.parent
|
||||
}
|
||||
if isMatch {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,4 +109,63 @@ struct SchemaNameUniquenessTests {
|
||||
<xs:element name="a"><xs:complexType><xs:attribute name="id" type="xs:string"/><xs:attribute name="k" type="xs:string"/></xs:complexType></xs:element>
|
||||
"""#))
|
||||
}
|
||||
|
||||
@Test("duplicate identity-constraint names across included files are rejected")
|
||||
func test_duplicateIdentityConstraintAcrossIncluded() {
|
||||
let imported = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="roota">
|
||||
<xs:complexType><xs:sequence/></xs:complexType>
|
||||
<xs:key name="KEY">
|
||||
<xs:selector xpath="person"/>
|
||||
<xs:field xpath="."/>
|
||||
</xs:key>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
let main = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:include schemaLocation="sub.xsd"/>
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence/></xs:complexType>
|
||||
<xs:key name="KEY">
|
||||
<xs:selector xpath="person"/>
|
||||
<xs:field xpath="."/>
|
||||
</xs:key>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect((try? PureXML.Schema.Document(main, schemaLoader: { $0 == "sub.xsd" ? imported : nil })) == nil)
|
||||
}
|
||||
|
||||
@Test("duplicate global element names across included files are rejected")
|
||||
func test_duplicateGlobalElementAcrossIncluded() {
|
||||
let imported = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="dup" type="xs:string"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let main = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:include schemaLocation="sub.xsd"/>
|
||||
<xs:element name="dup" type="xs:int"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect((try? PureXML.Schema.Document(main, schemaLoader: { $0 == "sub.xsd" ? imported : nil })) == nil)
|
||||
}
|
||||
|
||||
@Test("debug name00101m2")
|
||||
func test_debugName() {
|
||||
let path = "/private/tmp/xsts/xmlschema2006-11-06/sunData/IdConstrDefs/name/name00101m/name00101m2.xsd"
|
||||
guard let source = try? String(contentsOfFile: path) else {
|
||||
print("unreadable file")
|
||||
return
|
||||
}
|
||||
do {
|
||||
_ = try PureXML.Schema.Document(source)
|
||||
print("COMPILE SUCCESS")
|
||||
} catch {
|
||||
print("COMPILE FAILURE: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = 316
|
||||
private let knownSchemaInvalidAccepted = 309
|
||||
private let knownInstanceValidRejected = 171
|
||||
private let knownInstanceInvalidAccepted = 155
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open.
|
||||
| Bucket | Count | Reading |
|
||||
|---|---|---|
|
||||
| valid-schemas-rejected | **72 -> 4** | production target is 0: rejecting a correct schema is the most user-hostile defect (#148). Particle-restriction over-rejection fixed: pointless `maxOccurs=0` particles removed, content-free restriction of an EMPTY base, empty-derived emptiable-base, and (#163) the pairwise check over the normalized content model with occurrence subsumption applied only to same-kind pairings (cross-kind element/group, group/element, group/wildcard match occurrence inside, by effective total range / member). #164 resolved element-ref namespaces by prefix binding (the `particlesJj/Jk/Jm/Jn/Js`, `particlesQ/R` cluster, -28). Content-free (`maxOccurs=0`) derived groups and substitution-group declaration order closed `particlesW006`/`elemZ027_a`; the W3C MapAndSum rule (Sequence:Choice, #165) closed `particlesV003`. Remaining 1 (`particlesZ001`): a documented spec ambiguity (RecurseAsIfGroup; the reference rejects it), not a defect. |
|
||||
| **invalid-schemas-accepted** | **2461 -> 316** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion, elementTypeRestrictionOK NameAndTypeOK union checks, prohibited attribute type-check bypass, keyref arity verification, top-level group/attributeGroup local-attribute checks, simpleType restriction structure and prefix binding checks, namespace-aware reference resolution checks, simpleType variety constraints on list itemType and union memberTypes, targetNamespace scope matching on local schema references, substitution group exclusions based on element final and schema finalDefault attributes landed). |
|
||||
| **invalid-schemas-accepted** | **2461 -> 309** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion, elementTypeRestrictionOK NameAndTypeOK union checks, prohibited attribute type-check bypass, keyref arity verification, top-level group/attributeGroup local-attribute checks, simpleType restriction structure and prefix binding checks, namespace-aware reference resolution checks, simpleType variety constraints on list itemType and union memberTypes, targetNamespace scope matching on local schema references, substitution group exclusions based on element final and schema finalDefault attributes, cross-container component-name and identity-constraint uniqueness checks landed). |
|
||||
| valid-instances-rejected | **233 -> 171** | (instance side; #164's element-ref namespace fix is shared with instance validation, complexContent restriction attributes inheritance and namespace fixes) |
|
||||
| invalid-instances-accepted | **160 -> 155** | (instance side; resolving inline `<simpleType>` restriction base, complexContent restriction attributes prohibition) |
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user